diff --git a/cli/util.zig b/cli/util.zig index c04aab5..2580e91 100644 --- a/cli/util.zig +++ b/cli/util.zig @@ -37,7 +37,7 @@ pub fn printFailure() void { const PrintContext = enum { success, failure }; /// Print some output in with a given context to stderr. pub fn print(comptime context: PrintContext, comptime message: []const u8, args: anytype) !void { - const writer = std.io.getStdErr().writer(); + const writer = stderrAnsiWriter(); switch (context) { .success => try writer.print( std.fmt.comptimePrint("{s} {s}\n", .{ icons.check, colors.green(message) }), @@ -50,6 +50,51 @@ pub fn print(comptime context: PrintContext, comptime message: []const u8, args: } } +/// A stderr writer that translates ANSI escape codes to Windows console API +/// calls. On non-Windows, writes pass through directly to stderr. +const StderrAnsiWriter = struct { + pub fn writeAll(_: StderrAnsiWriter, bytes: []const u8) !void { + const stderr = std.io.getStdErr(); + try writeAnsi(stderr, stderr.writer(), bytes); + } + + pub fn writeByte(self: StderrAnsiWriter, byte: u8) !void { + try self.writeAll(&.{byte}); + } + + pub fn print(self: StderrAnsiWriter, comptime format: []const u8, args: anytype) !void { + try std.fmt.format(self, format, args); + } +}; + +fn stderrAnsiWriter() StderrAnsiWriter { + return .{}; +} + +/// Write a string of bytes, possibly containing ANSI escape codes. Translate +/// ANSI escape codes into Windows API console commands. In non-Windows +/// environments, output ANSI bytes directly. +fn writeAnsi(file: std.fs.File, writer: anytype, text: []const u8) !void { + if (builtin.os.tag != .windows) { + try writer.writeAll(text); + } else { + var it = std.mem.tokenizeSequence(u8, text, "\x1b["); + while (it.next()) |token| { + if (std.mem.indexOfScalar(u8, token, 'm')) |index| { + if (index > 0 and index + 1 < token.len) { + if (colors.windows_map.get(token[0..index])) |color| { + try std.os.windows.SetConsoleTextAttribute(file.handle, color); + try writer.writeAll(token[index + 1 ..]); + continue; + } + } + } + // Fallback + try writer.writeAll(token); + } + } +} + /// Detects a Jetzig project directory either in the current directory or one of its parent /// directories. pub fn detectJetzigProjectDir() !std.fs.Dir { diff --git a/src/test_runner.zig b/src/test_runner.zig index 02aa8ef..2a8645b 100644 --- a/src/test_runner.zig +++ b/src/test_runner.zig @@ -18,6 +18,28 @@ pub fn log( jetzig.testing.logger.log(message_level, scope, format, args); } +/// A stderr writer that translates ANSI escape codes to Windows console API +/// calls via `jetzig.util.writeAnsi`. On non-Windows, writes pass through +/// directly to stderr. +const StderrAnsiWriter = struct { + pub fn writeAll(_: StderrAnsiWriter, bytes: []const u8) !void { + const stderr = std.io.getStdErr(); + try jetzig.util.writeAnsi(stderr, stderr.writer(), bytes); + } + + pub fn writeByte(self: StderrAnsiWriter, byte: u8) !void { + try self.writeAll(&.{byte}); + } + + pub fn print(self: StderrAnsiWriter, comptime format: []const u8, args: anytype) !void { + try std.fmt.format(self, format, args); + } +}; + +fn stderrAnsiWriter() StderrAnsiWriter { + return .{}; +} + const Test = struct { name: []const u8, function: TestFn, @@ -83,9 +105,7 @@ const Test = struct { } else null; } - pub fn print(self: Test, stream: anytype) !void { - const writer = stream.writer(); - + pub fn print(self: Test, writer: anytype) !void { switch (self.result) { .success => { try self.printPassed(writer); @@ -177,7 +197,7 @@ pub fn main() !void { try mime_map.build(); jetzig.testing.mime_map = &mime_map; - try std.io.getStdErr().writer().writeAll("\n[jetzig] Launching Test Runner...\n\n"); + try stderrAnsiWriter().writeAll("\n[jetzig] Launching Test Runner...\n\n"); jetzig.testing.logger = jetzig.testing.Logger.init(allocator); jetzig.testing.state = .ready; @@ -186,7 +206,7 @@ pub fn main() !void { jetzig.testing.logger.index = index; var t = Test.init(test_function); try t.run(allocator); - try t.print(std.io.getStdErr()); + try t.print(stderrAnsiWriter()); try tests.append(t); } @@ -210,7 +230,7 @@ fn printSummary(tests: []const Test, start: i128) !void { const tick = jetzig.colors.green("✔"); const cross = jetzig.colors.red("✗"); - const writer = std.io.getStdErr().writer(); + const writer = stderrAnsiWriter(); var total_duration_buf: [256]u8 = undefined; const total_duration = try jetzig.colors.duration(