diff --git a/Sources/CLTLogger.swift b/Sources/CLTLogger.swift index 6cc0aa1..e0b7de7 100644 --- a/Sources/CLTLogger.swift +++ b/Sources/CLTLogger.swift @@ -1,4 +1,7 @@ import Foundation +#if canImport(WinSDK) +import WinSDK +#endif import Logging @@ -188,8 +191,8 @@ public struct CLTLogger : LogHandler { } private static func autoLogStyle(with fh: FileHandle) -> Style { - if let s = getenv("CLTLOGGER_LOG_STYLE") { - switch String(cString: s) { + if let s = ProcessInfo.processInfo.environment["CLTLOGGER_LOG_STYLE"] { + switch s { case "none": return .none case "color": return .color case "emoji": return .emoji @@ -200,6 +203,11 @@ public struct CLTLogger : LogHandler { /* * * The logging style is not defined specifically in the dedicated environment value: we try and detect a correct value depending on other environmental clues. * * */ + if ProcessInfo.processInfo.environment["GITHUB_ACTIONS"] == "true" { + /* GitHub does support colors. */ + return .color + } +#if !os(Windows) /* Is the fd on which we write a tty? * Most ttys nowadays support colors, with a notable exception: Xcode. */ if isatty(fh.fileDescriptor) != 0 { @@ -215,12 +223,12 @@ public struct CLTLogger : LogHandler { } /* If the TERM env var is not set we assume colors are not supported and return the text logging style. * In theory we should use the curses database to check for colors (ncurses has the `has_colors` function for this). */ - return (getenv("TERM") == nil ? .text : .color) + return (ProcessInfo.processInfo.environment["TERM"] == nil ? .text : .color) } - if let s = getenv("GITHUB_ACTIONS"), String(cString: s) == "true" { - /* GitHub does support colors. */ - return .color +#else + if GetFileType(fh._handle) == FILE_TYPE_CHAR { } +#endif /* Unknown case: we return the text logging style. */ return .text } @@ -263,41 +271,31 @@ public extension CLTLogger { }() static func defaultConstantsByLogLevelForEmoji(on fh: FileHandle) -> [Logger.Level: Constants] { - func addMeta(_ str: String, _ padding: String) -> Constants { - var str = str - if isatty(fh.fileDescriptor) != 0, tcgetpgrp(fh.fileDescriptor) == -1, errno == ENOTTY { - /* We’re in Xcode (probably). - * By default we do not do the emoji padding, unless explicitly asked to (`CLTLOGGER_TERMINAL_EMOJI` set to anything but “NO”). */ - if let s = getenv("CLTLOGGER_TERMINAL_EMOJI"), String(cString: s) != "NO" { - str = str + padding - } - } else { - /* We’re not in Xcode (probably). - * By default we do the emoji padding, unless explicitly asked not to (`CLTLOGGER_TERMINAL_EMOJI` set to “NO”). */ - if let s = getenv("CLTLOGGER_TERMINAL_EMOJI"), String(cString: s) == "NO" { - /*nop*/ - } else { - str = str + padding - } - } + func addMeta(_ paddedEmoji: String) -> Constants { return .init( - logPrefix: str + " → ", - multilineLogPrefix: str + " ", + logPrefix: paddedEmoji + " → ", + multilineLogPrefix: paddedEmoji + " ", metadataLinePrefix: " ▷ ", metadataSeparator: " - ", logAndMetadataSeparator: " -- ", lineSeparator: "\n" ) } - /* The padding corrects alignment issues on the Terminal. */ + 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)) |") + //} return [ - .trace: addMeta("💩", ""), - .debug: addMeta("⚙️", " "), - .info: addMeta("📔", ""), - .notice: addMeta("🗣", " "), - .warning: addMeta("⚠️", " "), - .error: addMeta("❗️", ""), - .critical: addMeta("‼️", " ") + .trace: addMeta(emojiSet.paddedEmoji(for: .trace, in: outputEnvironment)), + .debug: addMeta(emojiSet.paddedEmoji(for: .debug, in: outputEnvironment)), + .info: addMeta(emojiSet.paddedEmoji(for: .info, in: outputEnvironment)), + .notice: addMeta(emojiSet.paddedEmoji(for: .notice, in: outputEnvironment)), + .warning: addMeta(emojiSet.paddedEmoji(for: .warning, in: outputEnvironment)), + .error: addMeta(emojiSet.paddedEmoji(for: .error, in: outputEnvironment)), + .critical: addMeta(emojiSet.paddedEmoji(for: .critical, in: outputEnvironment)), ] } diff --git a/Sources/Emoji.swift b/Sources/Emoji.swift new file mode 100644 index 0000000..8a42f15 --- /dev/null +++ b/Sources/Emoji.swift @@ -0,0 +1,82 @@ +import Foundation + + + +internal enum Emoji : String, CaseIterable { + + case poo = "💩" + case cog = "⚙️" + case notebook = "📔" + case speaker = "🗣" + case warning = "⚠️" + case exclamationPoint = "❗️" + case doubleExclamationPoint = "‼️" + case eyebrow = "🤨" + 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 = "🩵" + + 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, + .orangeHeart, .yellowHeart, .greenHeart, .blueHeart, .purpleHeart, + .blackHeart, .brownHeart, .whiteHeart: + return "" + + case .ambulance, .ladybug, .monocle, .greenCheck, .fearFace: + return "" + + case .cog, .warning, .doubleExclamationPoint, .redHeart: + guard !environment.isVSCode, environment != .macOSTerminal + else {return " "} + return "" + + case .speaker: + guard !environment.isVSCode, !environment.isWindowsShell, environment != .macOSTerminal, environment != .macOSiTerm2 + 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. */ + guard !environment.isWindowsShell + else {return ""/*negative one space*/} + return "" + + case .greyHeart, .pinkHeart, .lightBlueHeart: + guard !environment.isVSCode + else {return " "} + return "" + } + } + + func valueWithPadding(for environment: OutputEnvironment) -> String { + rawValue + padding(for: environment) + } + +} diff --git a/Sources/EmojiSet.swift b/Sources/EmojiSet.swift new file mode 100644 index 0000000..c56ec5c --- /dev/null +++ b/Sources/EmojiSet.swift @@ -0,0 +1,113 @@ +import Foundation + +import Logging + + + +internal enum EmojiSet : String { + + /** + 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 vaibhavsingh97EmojiLogger = "VAIBHAVSINGH97_EMOJI_LOGGER" + case vaibhavsingh97EmojiLoggerForVSCodeMacOS = "VAIBHAVSINGH97_EMOJI_LOGGER+VSCODE_MACOS" + + 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 + } + } + + /* 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. */ + } + } + + switch self { + case .original: + return original(logLevel) + + case .originalForWindowsTerminal: + switch logLevel { + case .critical: return .policeLight + case .error: return .redCross + default: return original(logLevel) + } + + case .originalForVSCodeMacOS: + switch logLevel { + case .critical: return .policeLight + case .warning: return .orangeDiamond + case .debug: return .worm + default: return original(logLevel) + } + + case .originalForVSCodeWindows: + switch logLevel { + case .critical: return .policeLight + case .notice: return .eyebrow + default: return original(logLevel) + } + + case .vaibhavsingh97EmojiLogger: + return vaibhavsingh97(logLevel) + + case .vaibhavsingh97EmojiLoggerForVSCodeMacOS: + switch logLevel { + case .warning: return .orangeDiamond + default: return vaibhavsingh97(logLevel) + } + } + } + + func paddedEmoji(for logLevel: Logger.Level, in environment: OutputEnvironment) -> String { + return emoji(for: logLevel).valueWithPadding(for: environment) + } + +} diff --git a/Sources/OutputEnvironment.swift b/Sources/OutputEnvironment.swift new file mode 100644 index 0000000..e2291f3 --- /dev/null +++ b/Sources/OutputEnvironment.swift @@ -0,0 +1,88 @@ +import Foundation + + + +internal enum OutputEnvironment : String { + + case xcode = "XCODE" + + case macOSTerminal = "MACOS_TERMINAL" + case macOSiTerm2 = "MACOS_ITERM2" + case macOSVSCode = "MACOS_VSCODE" + case macOSUnknown = "MACOS_UNKNOWN" + + /* This value is never auto-detected. + * We don’t know how to detect the Windows Terminal (TERM_PROGRAM is not set). */ + case windowsTerminal = "WINDOWS_TERMINAL" + /* This value is never auto-detected. + * We don’t know how to detect the Windows Console. */ + case windowsConsole = "WINDOWS_CONSOLE" + case windowsVSCode = "WINDOWS_VSCODE" + case windowsUnknown = "WINDOWS_UNKNOWN" + + case unknownVSCode = "UNKNOWN_VSCODE" + case unknown = "UNKNOWN" + + var isVSCode: Bool { + switch self { + case .macOSVSCode, .windowsVSCode, .unknownVSCode: return true + default: return false + } + } + + var isWindowsShell: Bool { + switch self { + case .windowsTerminal, .windowsConsole, .windowsUnknown: return true + default: return false + } + } + + static func detect(from fh: FileHandle, _ envVars: [String: String] = ProcessInfo.processInfo.environment) -> OutputEnvironment { + if let envStr = envVars["CLTLOGGER_OUTPUT_ENV"] { + return OutputEnvironment(rawValue: envStr) ?? .unknown + } + +#if !os(Windows) + /* Let’s detect Xcode. */ + if isatty(fh.fileDescriptor) != 0 && tcgetpgrp(fh.fileDescriptor) == -1 && errno == ENOTTY { + return .xcode + } +#endif + switch envVars["TERM_PROGRAM"] { + case "Apple_Terminal": +#if os(macOS) + return .macOSTerminal +#else + return .unknown +#endif + + case "iTerm.app": +#if os(macOS) + return .macOSiTerm2 +#else + return .unknown +#endif + + case "vscode": +#if os(macOS) + return .macOSVSCode +#elseif os(Windows) + return .windowsVSCode +#else + return .unknownVSCode +#endif + + default: +#if os(macOS) + return .macOSUnknown +#elseif os(Windows) + /* We don’t know how to detect the Windows Terminal env: + * anything we have not previously detected on Windows is the Terminal. */ + return .windowsTerminal +#else + return .unknown +#endif + } + } + +}