/// *zlog* defaultLog function replacement, which adjusts the surrounding contents of every `std.log` message. fn logFn( comptime message_level: log.Level, comptime scope: @EnumLiteral(), comptime format: []const u8, args: anytype, ) void { var buf: [128]u8 = undefined; if (comptime build_options.file.len > 0) { const prefix = if (scope == .default) " " else "(" ++ @tagName(scope) ++ ") "; const level_txt = switch (comptime message_level) { .err => "[error]", .warn => "[warning]", .info => "[info]", .debug => "[debug]", }; // TODO let user configure the format he wants to use for logging and use a pretty good default one? const complete_format = level_txt ++ prefix ++ format ++ "\n"; // TODO use common logging format, such that in integrates well with other logging frameworks // (i.e. golang's logger, log4j, etc.) const fd = std.posix.open(build_options.file, .{ .CREAT = true, .APPEND = true, .ACCMODE = .WRONLY, }, 0o600) catch @panic("Could not append to log file"); defer std.posix.close(fd); var buffer = std.Io.File.Writer.init(.{ .handle = fd }, std.Options.debug_io, &buf); var writer = &buffer.interface; defer writer.flush() catch unreachable; log_writing(writer, complete_format, false, args); } if (comptime build_options.stderr) { const prefix = if (scope == .default) " " else "(\x1b[2m" ++ @tagName(scope) ++ "\x1b[0m) "; const level_txt = switch (comptime message_level) { .err => "[\x1b[38;5;9merror\x1b[0m]", .warn => "[\x1b[38;5;11mwarning\x1b[0m]", .info => "[\x1b[38;5;10minfo\x1b[0m]", .debug => "[\x1b[38;5;12mdebug\x1b[0m]", }; const complete_format = level_txt ++ prefix ++ format ++ "\n"; const io = std.debug.lockStderr(&buf); defer std.debug.unlockStderr(); var writer = &io.file_writer.interface; defer writer.flush() catch unreachable; log_writing(writer, complete_format, true, args); } } inline fn log_writing(writer: *std.Io.Writer, comptime format: []const u8, comptime ansi: bool, args: anytype) void { nosuspend { if (build_options.timestamp) log_timestamp(writer, ansi); writer.print(format, args) catch return; } } /// Prepend the current timestamp in the following format: /// `// :: ` /// /// NOTE all information are displayed using numbers inline fn log_timestamp(writer: anytype, comptime ansi: bool) void { const time = std.posix.clock_gettime(.REALTIME) catch @panic("Cannot request timestamp"); const epoch_secs: std.time.epoch.EpochSeconds = .{ .secs = @intCast(time.sec) }; const day_secs = epoch_secs.getDaySeconds(); const year_and_day = epoch_secs.getEpochDay().calculateYearDay(); const month_and_day = year_and_day.calculateMonthDay(); writer.print( if (comptime ansi) "\x1b[1m{d}/{d:0>2}/{d:0>2} {d:0>2}:{d:0>2}:{d:0>2}\x1b[0m " else "{d}/{d:0>2}/{d:0>2} {d:0>2}:{d:0>2}:{d:0>2} ", .{ year_and_day.year, month_and_day.month.numeric(), month_and_day.day_index + 1, day_secs.getHoursIntoDay(), day_secs.getMinutesIntoHour(), day_secs.getSecondsIntoMinute(), }, ) catch return; } pub fn pretty_format(object: anytype, comptime format: []const u8, options: fmt.FormatOptions, writer: anytype) !void { try inner_format(object, format, options, writer, 0); } fn inner_format(object: anytype, comptime format: []const u8, options: fmt.FormatOptions, writer: anytype, comptime depth: u8) !void { const Object = @TypeOf(object); const object_info = @typeInfo(Object); switch (object_info) { .@"struct" => |s| { try writer.writeAll(@typeName(Object)); try writer.writeAll(" = struct {\n"); inline for (s.fields) |field| { try writer.writeAll("\t" ** (depth + 1)); try writer.writeAll("."); try writer.writeAll(field.name); try writer.writeAll(" = "); // TODO check corresponding type and try to adapt fmt accordingly! // TODO pass along the already parsed formatting options too try inner_format(@field(object, field.name), format, options, writer, depth + 1); try writer.writeAll(",\n"); } try writer.writeAll("\t" ** depth); try writer.writeAll("}"); }, .@"enum" => |e| { try writer.writeAll(@typeName(Object)); try writer.writeAll(" = enum {\n"); inline for (e.fields) |field| { try writer.writeAll("\t" ** (depth + 1)); try writer.writeAll(field.name); try writer.writeAll(",\n"); } try writer.writeAll("\t" ** depth); try writer.writeAll("} = ."); try writer.writeAll(@tagName(object)); }, .@"union" => |u| { try writer.writeAll(@typeName(Object)); try writer.writeAll(" = union"); if (u.tag_type) |Tag| { _ = Tag; try writer.writeAll("(enum)"); } try writer.writeAll(" {\n"); inline for (u.fields) |field| { try writer.writeAll("\t" ** (depth + 1)); try writer.writeAll(field.name); if (@typeInfo(field.type) != .void) { try writer.writeAll(": "); try writer.writeAll(@typeName(field.type)); } try writer.writeAll(",\n"); } try writer.writeAll("\t" ** depth); try writer.writeAll("} = "); if (u.tag_type) |Tag| { try writer.writeAll(".{ ."); try writer.writeAll(@tagName(object)); try writer.writeAll(" = "); inline for (u.fields) |u_field| if (object == @field(Tag, u_field.name)) { try inner_format(@field(object, u_field.name), format, options, writer, depth + 1); }; try writer.writeAll(" }"); } else { // NOTE the value of a union (untagged) is displayed like this also in the standard library, // not sure if you can reflect the used variant (and its value) try fmt.format(writer, "@{x}", .{@intFromPtr(&object)}); } }, .pointer => |p| switch (p.size) { .slice => switch (@typeInfo(p.child)) { .int => |num| { if (num.signedness != .unsigned) { try fmt.format(writer, "[]{s}: {any}", .{ @typeName(p.child), object }); } else { switch (num.bits) { 8 => try fmt.format(writer, "\"{s}\"", .{object}), else => try fmt.format(writer, "[]{s}: {any}", .{ @typeName(p.child), object }), } } }, else => try fmt.format(writer, "[]{s}: {any}", .{ @typeName(p.child), object }), }, .c => switch (@typeInfo(p.child)) { .int => |num| { if (num.signedness != .unsigned) { try fmt.format(writer, "[*c]{s}: {any}", .{ @typeName(p.child), object }); } else { switch (num.bits) { 8 => try fmt.format(writer, "\"{s}\"", .{object}), else => try fmt.format(writer, "[*c]{s}: {any}", .{ @typeName(p.child), object }), } } }, else => try fmt.format(writer, "[]{s}: {any}", .{ @typeName(p.child), object }), }, .many => try fmt.format(writer, "[*]{s}: {any}", .{ @typeName(p.child), object }), .one => try fmt.format(writer, "[1]{s}: {any}", .{ @typeName(p.child), object }), }, .array => |a| switch (@typeInfo(a.child)) { .int => |num| { if (num.signedness != .unsigned) { try fmt.format(writer, "[]{s}: {any}", .{ @typeName(a.child), object }); } else { switch (num.bits) { 8 => try fmt.format(writer, "\"{s}\"", .{object}), else => try fmt.format(writer, "[]{s}: {any}", .{ @typeName(a.child), object }), } } }, else => try fmt.format(writer, "[]{s}: {any}", .{ @typeName(a.child), object }), }, .vector => |v| switch (@typeInfo(v.child)) { .int => |num| { if (num.signedness != .unsigned) { try fmt.format(writer, "[]{s}: {any}", .{ @typeName(v.child), object }); } else { switch (num.bits) { 8 => try fmt.format(writer, "\"{s}\"", .{object}), else => try fmt.format(writer, "[]{s}: {any}", .{ @typeName(v.child), object }), } } }, else => try fmt.format(writer, "[]{s}: {any}", .{ @typeName(v.child), object }), }, else => try fmt.format(writer, format, .{object}), } } pub const std_options: std.Options = .{ .logFn = logFn, }; const std = @import("std"); const fs = std.fs; const log = std.log; const fmt = std.fmt; const build_options = @import("build_options");