From 2fbd2f2f6ac547dcb1a4ede2e6d92db72a30b5f0 Mon Sep 17 00:00:00 2001 From: Bob Farrell Date: Sun, 12 May 2024 14:09:28 +0100 Subject: [PATCH] WIP --- demo/src/main.zig | 2 +- src/jetzig/colors.zig | 123 ++++++++++++++++------- src/jetzig/http/Server.zig | 4 +- src/jetzig/loggers/DevelopmentLogger.zig | 34 +++---- src/jetzig/loggers/LogQueue.zig | 48 ++++++++- src/jetzig/middleware/HtmxMiddleware.zig | 5 +- 6 files changed, 150 insertions(+), 66 deletions(-) diff --git a/demo/src/main.zig b/demo/src/main.zig index b782436..941ec5a 100644 --- a/demo/src/main.zig +++ b/demo/src/main.zig @@ -12,7 +12,7 @@ pub const jetzig_options = struct { pub const middleware: []const type = &.{ // htmx middleware skips layouts when `HX-Target` header is present and issues // `HX-Redirect` instead of a regular HTTP redirect when `request.redirect` is called. - // jetzig.middleware.HtmxMiddleware, + jetzig.middleware.HtmxMiddleware, // Demo middleware included with new projects. Remove once you are familiar with Jetzig's // middleware system. // @import("app/middleware/DemoMiddleware.zig"), diff --git a/src/jetzig/colors.zig b/src/jetzig/colors.zig index d8e9960..5d74026 100644 --- a/src/jetzig/colors.zig +++ b/src/jetzig/colors.zig @@ -4,48 +4,83 @@ const builtin = @import("builtin"); const types = @import("types.zig"); +// Must be consistent with `std.io.tty.Color` for Windows compatibility. const codes = .{ .escape = "\x1b[", - .reset = "0;0", - .black = "0;30", - .red = "0;31", - .green = "0;32", - .yellow = "0;33", - .blue = "0;34", - .purple = "0;35", - .cyan = "0;36", - .white = "0;37", + .black = "30m", + .red = "31m", + .green = "32m", + .yellow = "33m", + .blue = "34m", + .magenta = "35m", + .cyan = "36m", + .white = "37m", + .bright_black = "90m", + .bright_red = "91m", + .bright_green = "92m", + .bright_yellow = "93m", + .bright_blue = "94m", + .bright_magenta = "95m", + .bright_cyan = "96m", + .bright_white = "97m", + .bold = "1m", + .dim = "2m", + .reset = "0m", }; -pub fn colorize(color: std.io.tty.Color, buf: []u8, input: []const u8, target: std.fs.File) ![]const u8 { - const config = std.io.tty.detectConfig(target); +/// Map color codes generated by `std.io.tty.Config.setColor` back to `std.io.tty.Color`. Used by +/// `jetzig.loggers.LogQueue.writeWindows` to parse escape codes so they can be passed to +/// `std.io.tty.Config.setColor` (using Windows API to set console color mode). +pub const codes_map = std.StaticStringMap(std.io.tty.Color).initComptime(.{ + .{ "30", .black }, + .{ "31", .red }, + .{ "32", .green }, + .{ "33", .yellow }, + .{ "34", .blue }, + .{ "35", .magenta }, + .{ "36", .cyan }, + .{ "37", .white }, + .{ "90", .bright_black }, + .{ "91", .bright_red }, + .{ "92", .bright_green }, + .{ "93", .bright_yellow }, + .{ "94", .bright_blue }, + .{ "95", .bright_magenta }, + .{ "96", .bright_cyan }, + .{ "97", .bright_white }, + .{ "1", .bold }, + .{ "2", .dim }, + .{ "0", .reset }, +}); + +/// Colorize a log message. Note that we force `.escape_codes` when we are a TTY even on Windows. +/// `jetzig.loggers.LogQueue` parses the ANSI codes and uses `std.io.tty.Config.setColor` to +/// invoke the appropriate Windows API call to set the terminal color before writing each token. +/// We must do it this way because Windows colors are set by API calls at the time of write, not +/// encoded into the message string. +pub fn colorize(color: std.io.tty.Color, buf: []u8, input: []const u8, is_colorized: bool) ![]const u8 { + if (!is_colorized) return input; + + const config: std.io.tty.Config = .escape_codes; var stream = std.io.fixedBufferStream(buf); const writer = stream.writer(); try config.setColor(writer, color); try writer.writeAll(input); - try config.setColor(writer, .white); + try config.setColor(writer, .reset); return stream.getWritten(); } fn wrap(comptime attribute: []const u8, comptime message: []const u8) []const u8 { - if (builtin.os.tag == .windows) { - return message; - } else { - return codes.escape ++ attribute ++ "m" ++ message ++ codes.escape ++ codes.reset ++ "m"; - } + return codes.escape ++ attribute ++ message ++ codes.escape ++ codes.reset; } fn runtimeWrap(allocator: std.mem.Allocator, attribute: []const u8, message: []const u8) ![]const u8 { - if (builtin.os.tag == .windows) { - return try allocator.dupe(u8, message); - } else { - return try std.mem.join( - allocator, - "", - &[_][]const u8{ codes.escape, attribute, "m", message, codes.escape, codes.reset, "m" }, - ); - } + return try std.mem.join( + allocator, + "", + &[_][]const u8{ codes.escape, attribute, message, codes.escape, codes.reset }, + ); } pub fn black(comptime message: []const u8) []const u8 { @@ -88,12 +123,12 @@ pub fn runtimeBlue(allocator: std.mem.Allocator, message: []const u8) ![]const u return try runtimeWrap(allocator, codes.blue, message); } -pub fn purple(comptime message: []const u8) []const u8 { - return wrap(codes.purple, message); +pub fn magenta(comptime message: []const u8) []const u8 { + return wrap(codes.magenta, message); } -pub fn runtimePurple(allocator: std.mem.Allocator, message: []const u8) ![]const u8 { - return try runtimeWrap(allocator, codes.purple, message); +pub fn runtimeMagenta(allocator: std.mem.Allocator, message: []const u8) ![]const u8 { + return try runtimeWrap(allocator, codes.magenta, message); } pub fn cyan(comptime message: []const u8) []const u8 { @@ -112,16 +147,26 @@ pub fn runtimeWhite(allocator: std.mem.Allocator, message: []const u8) ![]const return try runtimeWrap(allocator, codes.white, message); } -pub fn duration(buf: *[256]u8, delta: i64) ![]const u8 { - const code = if (delta < 1000000) - codes.green +pub fn duration(buf: *[256]u8, delta: i64, is_colorized: bool) ![]const u8 { + if (!is_colorized) { + return try std.fmt.bufPrint( + buf, + "{}", + .{std.fmt.fmtDurationSigned(delta)}, + ); + } + + const color: std.io.tty.Color = if (delta < 1000000) + .green else if (delta < 5000000) - codes.yellow + .yellow else - codes.red; - return try std.fmt.bufPrint( - buf, - "{s}{s}m{}{s}{s}m", - .{ codes.escape, code, std.fmt.fmtDurationSigned(delta), codes.escape, codes.reset }, + .red; + var duration_buf: [256]u8 = undefined; + const formatted_duration = try std.fmt.bufPrint( + &duration_buf, + "{}", + .{std.fmt.fmtDurationSigned(delta)}, ); + return try colorize(color, buf, formatted_duration, true); } diff --git a/src/jetzig/http/Server.zig b/src/jetzig/http/Server.zig index 9f42a84..879ee0b 100644 --- a/src/jetzig/http/Server.zig +++ b/src/jetzig/http/Server.zig @@ -107,6 +107,8 @@ fn processNextRequest( httpz_request: *httpz.Request, httpz_response: *httpz.Response, ) !void { + const start_time = std.time.nanoTimestamp(); + const state = try self.allocator.create(jetzig.http.Request.CallbackState); const arena = try self.allocator.create(std.heap.ArenaAllocator); arena.* = std.heap.ArenaAllocator.init(self.allocator); @@ -120,8 +122,6 @@ fn processNextRequest( const allocator = state.arena.allocator(); - const start_time = std.time.nanoTimestamp(); - var response = try jetzig.http.Response.init(allocator, httpz_response); var request = try jetzig.http.Request.init( allocator, diff --git a/src/jetzig/loggers/DevelopmentLogger.zig b/src/jetzig/loggers/DevelopmentLogger.zig index 70a9e19..2f9135f 100644 --- a/src/jetzig/loggers/DevelopmentLogger.zig +++ b/src/jetzig/loggers/DevelopmentLogger.zig @@ -44,10 +44,8 @@ pub fn log( var timestamp_buf: [256]u8 = undefined; const iso8601 = try timestamp.iso8601(×tamp_buf); - var level_buf: [16]u8 = undefined; - const formatted_level = try colorizedLogLevel(level, &level_buf, self.log_queue.reader.stdout_file); - const target = jetzig.loggers.logTarget(level); + const formatted_level = colorizedLogLevel(level); try self.log_queue.print( "{s: >5} [{s}] {s}\n", @@ -59,14 +57,11 @@ pub fn log( /// Log a one-liner including response status code, path, method, duration, etc. pub fn logRequest(self: DevelopmentLogger, request: *const jetzig.http.Request) !void { var duration_buf: [256]u8 = undefined; - const formatted_duration = if (self.stdout_colorized) - try jetzig.colors.duration(&duration_buf, jetzig.util.duration(request.start_time)) - else - try std.fmt.bufPrint( - &duration_buf, - "{}", - .{std.fmt.fmtDurationSigned(jetzig.util.duration(request.start_time))}, - ); + const formatted_duration = try jetzig.colors.duration( + &duration_buf, + jetzig.util.duration(request.start_time), + self.stdout_colorized, + ); const status: jetzig.http.status_codes.TaggedStatusCode = switch (request.response.status_code) { inline else => |status_code| @unionInit( @@ -85,8 +80,7 @@ pub fn logRequest(self: DevelopmentLogger, request: *const jetzig.http.Request) var timestamp_buf: [256]u8 = undefined; const iso8601 = try timestamp.iso8601(×tamp_buf); - var level_buf: [16]u8 = undefined; - const formatted_level = try colorizedLogLevel(.INFO, &level_buf, self.log_queue.reader.stdout_file); + const formatted_level = if (self.stdout_colorized) colorizedLogLevel(.INFO) else @tagName(.INFO); try self.log_queue.print("{s: >5} [{s}] [{s}/{s}/{s}] {s}\n", .{ formatted_level, @@ -98,13 +92,13 @@ pub fn logRequest(self: DevelopmentLogger, request: *const jetzig.http.Request) }, .stdout); } -fn colorizedLogLevel(comptime level: LogLevel, buf: []u8, file: std.fs.File) ![]const u8 { +inline fn colorizedLogLevel(comptime level: LogLevel) []const u8 { return switch (level) { - .TRACE => jetzig.colors.colorize(.white, buf, @tagName(level), file), - .DEBUG => jetzig.colors.colorize(.cyan, buf, @tagName(level), file), - .INFO => jetzig.colors.colorize(.blue, buf, @tagName(level) ++ " ", file), - .WARN => jetzig.colors.colorize(.yellow, buf, @tagName(level) ++ " ", file), - .ERROR => jetzig.colors.colorize(.red, buf, @tagName(level), file), - .FATAL => jetzig.colors.colorize(.red, buf, @tagName(level), file), + .TRACE => jetzig.colors.white(@tagName(level)), + .DEBUG => jetzig.colors.cyan(@tagName(level)), + .INFO => jetzig.colors.blue(@tagName(level) ++ " "), + .WARN => jetzig.colors.yellow(@tagName(level) ++ " "), + .ERROR => jetzig.colors.red(@tagName(level)), + .FATAL => jetzig.colors.red(@tagName(level)), }; } diff --git a/src/jetzig/loggers/LogQueue.zig b/src/jetzig/loggers/LogQueue.zig index 5d6350d..27343ce 100644 --- a/src/jetzig/loggers/LogQueue.zig +++ b/src/jetzig/loggers/LogQueue.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const builtin = @import("builtin"); const jetzig = @import("../../jetzig.zig"); @@ -81,8 +82,10 @@ pub fn setFiles(self: *LogQueue, stdout_file: std.fs.File, stderr_file: std.fs.F }; self.stdout_is_tty = stdout_file.isTty(); self.stderr_is_tty = stderr_file.isTty(); + self.stdout_colorize = std.io.tty.detectConfig(stdout_file) != .no_color; self.stderr_colorize = std.io.tty.detectConfig(stderr_file) != .no_color; + self.state = .ready; } @@ -164,6 +167,8 @@ pub const Reader = struct { var stdout_written = false; var stderr_written = false; + var file: std.fs.File = undefined; + var colorize = false; while (try self.queue.popFirst()) |event| { self.queue.writer.mutex.lock(); @@ -172,10 +177,18 @@ pub const Reader = struct { const writer = switch (event.target) { .stdout => blk: { stdout_written = true; + if (builtin.os.tag == .windows) { + file = self.stdout_file; + colorize = self.queue.stdout_colorize; + } break :blk stdout_writer; }, .stderr => blk: { stderr_written = true; + if (builtin.os.tag == .windows) { + file = self.stderr_file; + colorize = self.queue.stderr_colorize; + } break :blk stderr_writer; }, }; @@ -187,7 +200,11 @@ pub const Reader = struct { continue; } - try writer.writeAll(event.message[0..event.len]); + if (builtin.os.tag == .windows and colorize) { + try writeWindows(file, writer, event); + } else { + try writer.writeAll(event.message[0..event.len]); + } self.queue.writer.position -= 1; @@ -244,6 +261,35 @@ fn popFirst(self: *LogQueue) !?Event { } } +fn writeWindows(file: std.fs.File, writer: anytype, event: Event) !void { + var info: std.os.windows.CONSOLE_SCREEN_BUFFER_INFO = undefined; + const config: std.io.tty.Config = if (std.os.windows.kernel32.GetConsoleScreenBufferInfo( + file.handle, + &info, + ) != std.os.windows.TRUE) + .no_color + else + .{ .windows_api = .{ + .handle = file.handle, + .reset_attributes = info.wAttributes, + } }; + + var it = std.mem.tokenizeSequence(u8, event.message[0..event.len], "\x1b["); + while (it.next()) |token| { + if (std.mem.indexOfScalar(u8, token, 'm')) |index| { + if (index > 0 and index + 1 < token.len) { + if (jetzig.colors.codes_map.get(token[0..index])) |color| { + try config.setColor(writer, color); + try writer.writeAll(token[index + 1 ..]); + continue; + } + } + } + // Fallback + try writer.writeAll(token); + } +} + test "print to stdout and stderr" { var log_queue = LogQueue.init(std.testing.allocator); defer log_queue.deinit(); diff --git a/src/jetzig/middleware/HtmxMiddleware.zig b/src/jetzig/middleware/HtmxMiddleware.zig index ebedcfe..7334077 100644 --- a/src/jetzig/middleware/HtmxMiddleware.zig +++ b/src/jetzig/middleware/HtmxMiddleware.zig @@ -29,10 +29,9 @@ pub fn afterRequest(self: *Self, request: *jetzig.http.Request) !void { pub fn beforeResponse(self: *Self, request: *jetzig.http.Request, response: *jetzig.http.Response) !void { _ = self; if (response.status_code != .moved_permanently and response.status_code != .found) return; - if (request.headers.getFirstValue("HX-Request") == null) return; + if (request.headers.get("HX-Request") == null) return; - if (response.headers.getFirstValue("Location")) |location| { - response.headers.remove("Location"); + if (response.headers.get("Location")) |location| { response.status_code = .ok; request.response_data.reset(); try response.headers.append("HX-Redirect", location);