From 0f6fa3f20b3b28958921bd63a9a9d96468455e9c Mon Sep 17 00:00:00 2001 From: Linus Groh Date: Sun, 21 May 2023 14:27:28 +0100 Subject: [PATCH] std: Move std.debug.{TTY.Config,detectTTYConfig} to std.io.tty Also get rid of the TTY wrapper struct, which was exlusively used as a namespace - this is done by the tty.zig root struct now. detectTTYConfig has been renamed to just detectConfig, which is enough given the new namespace. Additionally, a doc comment had been added. --- lib/build_runner.zig | 12 ++-- lib/std/Build.zig | 2 +- lib/std/Build/Step.zig | 2 +- lib/std/builtin.zig | 2 +- lib/std/debug.zig | 137 +++--------------------------------- lib/std/io.zig | 2 + lib/std/io/tty.zig | 121 +++++++++++++++++++++++++++++++ lib/std/testing.zig | 8 +-- lib/std/zig/ErrorBundle.zig | 4 +- src/main.zig | 4 +- test/src/Cases.zig | 2 +- 11 files changed, 152 insertions(+), 144 deletions(-) create mode 100644 lib/std/io/tty.zig diff --git a/lib/build_runner.zig b/lib/build_runner.zig index 7eec164871..a09ec2cf1f 100644 --- a/lib/build_runner.zig +++ b/lib/build_runner.zig @@ -333,7 +333,7 @@ const Run = struct { claimed_rss: usize, enable_summary: ?bool, - ttyconf: std.debug.TTY.Config, + ttyconf: std.io.tty.Config, stderr: std.fs.File, }; @@ -535,7 +535,7 @@ const PrintNode = struct { last: bool = false, }; -fn printPrefix(node: *PrintNode, stderr: std.fs.File, ttyconf: std.debug.TTY.Config) !void { +fn printPrefix(node: *PrintNode, stderr: std.fs.File, ttyconf: std.io.tty.Config) !void { const parent = node.parent orelse return; if (parent.parent == null) return; try printPrefix(parent, stderr, ttyconf); @@ -553,7 +553,7 @@ fn printTreeStep( b: *std.Build, s: *Step, stderr: std.fs.File, - ttyconf: std.debug.TTY.Config, + ttyconf: std.io.tty.Config, parent_node: *PrintNode, step_stack: *std.AutoArrayHashMapUnmanaged(*Step, void), ) !void { @@ -1026,15 +1026,15 @@ fn cleanExit() void { const Color = enum { auto, off, on }; -fn get_tty_conf(color: Color, stderr: std.fs.File) std.debug.TTY.Config { +fn get_tty_conf(color: Color, stderr: std.fs.File) std.io.tty.Config { return switch (color) { - .auto => std.debug.detectTTYConfig(stderr), + .auto => std.io.tty.detectConfig(stderr), .on => .escape_codes, .off => .no_color, }; } -fn renderOptions(ttyconf: std.debug.TTY.Config) std.zig.ErrorBundle.RenderOptions { +fn renderOptions(ttyconf: std.io.tty.Config) std.zig.ErrorBundle.RenderOptions { return .{ .ttyconf = ttyconf, .include_source_line = ttyconf != .no_color, diff --git a/lib/std/Build.zig b/lib/std/Build.zig index b36e815f72..bb642b5e66 100644 --- a/lib/std/Build.zig +++ b/lib/std/Build.zig @@ -1712,7 +1712,7 @@ fn dumpBadGetPathHelp( s.name, }); - const tty_config = std.debug.detectTTYConfig(stderr); + const tty_config = std.io.tty.detectConfig(stderr); tty_config.setColor(w, .red) catch {}; try stderr.writeAll(" The step was created by this stack trace:\n"); tty_config.setColor(w, .reset) catch {}; diff --git a/lib/std/Build/Step.zig b/lib/std/Build/Step.zig index 40c88df2b9..a0d7a6a296 100644 --- a/lib/std/Build/Step.zig +++ b/lib/std/Build/Step.zig @@ -237,7 +237,7 @@ pub fn dump(step: *Step) void { const stderr = std.io.getStdErr(); const w = stderr.writer(); - const tty_config = std.debug.detectTTYConfig(stderr); + const tty_config = std.io.tty.detectConfig(stderr); const debug_info = std.debug.getSelfDebugInfo() catch |err| { w.print("Unable to dump stack trace: Unable to open debug info: {s}\n", .{ @errorName(err), diff --git a/lib/std/builtin.zig b/lib/std/builtin.zig index 56fab05d88..710aaefd5a 100644 --- a/lib/std/builtin.zig +++ b/lib/std/builtin.zig @@ -51,7 +51,7 @@ pub const StackTrace = struct { const debug_info = std.debug.getSelfDebugInfo() catch |err| { return writer.print("\nUnable to print stack trace: Unable to open debug info: {s}\n", .{@errorName(err)}); }; - const tty_config = std.debug.detectTTYConfig(std.io.getStdErr()); + const tty_config = std.io.tty.detectConfig(std.io.getStdErr()); try writer.writeAll("\n"); std.debug.writeStackTrace(self, writer, arena.allocator(), debug_info, tty_config) catch |err| { try writer.print("Unable to print stack trace: {s}\n", .{@errorName(err)}); diff --git a/lib/std/debug.zig b/lib/std/debug.zig index d98cf8f27d..08407023d6 100644 --- a/lib/std/debug.zig +++ b/lib/std/debug.zig @@ -5,7 +5,6 @@ const mem = std.mem; const io = std.io; const os = std.os; const fs = std.fs; -const process = std.process; const testing = std.testing; const elf = std.elf; const DW = std.dwarf; @@ -109,31 +108,6 @@ pub fn getSelfDebugInfo() !*DebugInfo { } } -pub fn detectTTYConfig(file: std.fs.File) TTY.Config { - if (builtin.os.tag == .wasi) { - // Per https://github.com/WebAssembly/WASI/issues/162 ANSI codes - // aren't currently supported. - return .no_color; - } else if (process.hasEnvVarConstant("ZIG_DEBUG_COLOR")) { - return .escape_codes; - } else if (process.hasEnvVarConstant("NO_COLOR")) { - return .no_color; - } else if (file.supportsAnsiEscapeCodes()) { - return .escape_codes; - } else if (native_os == .windows and file.isTty()) { - var info: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined; - if (windows.kernel32.GetConsoleScreenBufferInfo(file.handle, &info) != windows.TRUE) { - // TODO: Should this return an error instead? - return .no_color; - } - return .{ .windows_api = .{ - .handle = file.handle, - .reset_attributes = info.wAttributes, - } }; - } - return .no_color; -} - /// Tries to print the current stack trace to stderr, unbuffered, and ignores any error returned. /// TODO multithreaded awareness pub fn dumpCurrentStackTrace(start_addr: ?usize) void { @@ -154,7 +128,7 @@ pub fn dumpCurrentStackTrace(start_addr: ?usize) void { stderr.print("Unable to dump stack trace: Unable to open debug info: {s}\n", .{@errorName(err)}) catch return; return; }; - writeCurrentStackTrace(stderr, debug_info, detectTTYConfig(io.getStdErr()), start_addr) catch |err| { + writeCurrentStackTrace(stderr, debug_info, io.tty.detectConfig(io.getStdErr()), start_addr) catch |err| { stderr.print("Unable to dump stack trace: {s}\n", .{@errorName(err)}) catch return; return; }; @@ -182,7 +156,7 @@ pub fn dumpStackTraceFromBase(bp: usize, ip: usize) void { stderr.print("Unable to dump stack trace: Unable to open debug info: {s}\n", .{@errorName(err)}) catch return; return; }; - const tty_config = detectTTYConfig(io.getStdErr()); + const tty_config = io.tty.detectConfig(io.getStdErr()); if (native_os == .windows) { writeCurrentStackTraceWindows(stderr, debug_info, tty_config, ip) catch return; return; @@ -265,7 +239,7 @@ pub fn dumpStackTrace(stack_trace: std.builtin.StackTrace) void { stderr.print("Unable to dump stack trace: Unable to open debug info: {s}\n", .{@errorName(err)}) catch return; return; }; - writeStackTrace(stack_trace, stderr, getDebugInfoAllocator(), debug_info, detectTTYConfig(io.getStdErr())) catch |err| { + writeStackTrace(stack_trace, stderr, getDebugInfoAllocator(), debug_info, io.tty.detectConfig(io.getStdErr())) catch |err| { stderr.print("Unable to dump stack trace: {s}\n", .{@errorName(err)}) catch return; return; }; @@ -403,7 +377,7 @@ pub fn writeStackTrace( out_stream: anytype, allocator: mem.Allocator, debug_info: *DebugInfo, - tty_config: TTY.Config, + tty_config: io.tty.Config, ) !void { _ = allocator; if (builtin.strip_debug_info) return error.MissingDebugInfo; @@ -562,7 +536,7 @@ pub const StackIterator = struct { pub fn writeCurrentStackTrace( out_stream: anytype, debug_info: *DebugInfo, - tty_config: TTY.Config, + tty_config: io.tty.Config, start_addr: ?usize, ) !void { if (native_os == .windows) { @@ -634,7 +608,7 @@ pub noinline fn walkStackWindows(addresses: []usize) usize { pub fn writeCurrentStackTraceWindows( out_stream: anytype, debug_info: *DebugInfo, - tty_config: TTY.Config, + tty_config: io.tty.Config, start_addr: ?usize, ) !void { var addr_buf: [1024]usize = undefined; @@ -651,95 +625,6 @@ pub fn writeCurrentStackTraceWindows( } } -/// Provides simple functionality for manipulating the terminal in some way, -/// for debugging purposes, such as coloring text, etc. -pub const TTY = struct { - pub const Color = enum { - red, - green, - yellow, - cyan, - white, - dim, - bold, - reset, - }; - - pub const Config = union(enum) { - no_color, - escape_codes, - windows_api: if (native_os == .windows) WindowsContext else void, - - pub const WindowsContext = struct { - handle: File.Handle, - reset_attributes: u16, - }; - - pub fn setColor(conf: Config, out_stream: anytype, color: Color) !void { - nosuspend switch (conf) { - .no_color => return, - .escape_codes => { - const color_string = switch (color) { - .red => "\x1b[31;1m", - .green => "\x1b[32;1m", - .yellow => "\x1b[33;1m", - .cyan => "\x1b[36;1m", - .white => "\x1b[37;1m", - .bold => "\x1b[1m", - .dim => "\x1b[2m", - .reset => "\x1b[0m", - }; - try out_stream.writeAll(color_string); - }, - .windows_api => |ctx| if (native_os == .windows) { - const attributes = switch (color) { - .red => windows.FOREGROUND_RED | windows.FOREGROUND_INTENSITY, - .green => windows.FOREGROUND_GREEN | windows.FOREGROUND_INTENSITY, - .yellow => windows.FOREGROUND_RED | windows.FOREGROUND_GREEN | windows.FOREGROUND_INTENSITY, - .cyan => windows.FOREGROUND_GREEN | windows.FOREGROUND_BLUE | windows.FOREGROUND_INTENSITY, - .white, .bold => windows.FOREGROUND_RED | windows.FOREGROUND_GREEN | windows.FOREGROUND_BLUE | windows.FOREGROUND_INTENSITY, - .dim => windows.FOREGROUND_INTENSITY, - .reset => ctx.reset_attributes, - }; - try windows.SetConsoleTextAttribute(ctx.handle, attributes); - } else { - unreachable; - }, - }; - } - - pub fn writeDEC(conf: Config, writer: anytype, codepoint: u8) !void { - const bytes = switch (conf) { - .no_color, .windows_api => switch (codepoint) { - 0x50...0x5e => @as(*const [1]u8, &codepoint), - 0x6a => "+", // ┘ - 0x6b => "+", // ┐ - 0x6c => "+", // ┌ - 0x6d => "+", // └ - 0x6e => "+", // ┼ - 0x71 => "-", // ─ - 0x74 => "+", // ├ - 0x75 => "+", // ┤ - 0x76 => "+", // ┴ - 0x77 => "+", // ┬ - 0x78 => "|", // │ - else => " ", // TODO - }, - .escape_codes => switch (codepoint) { - // Here we avoid writing the DEC beginning sequence and - // ending sequence in separate syscalls by putting the - // beginning and ending sequence into the same string - // literals, to prevent terminals ending up in bad states - // in case a crash happens between syscalls. - inline 0x50...0x7f => |x| "\x1B\x28\x30" ++ [1]u8{x} ++ "\x1B\x28\x42", - else => unreachable, - }, - }; - return writer.writeAll(bytes); - } - }; -}; - fn machoSearchSymbols(symbols: []const MachoSymbol, address: usize) ?*const MachoSymbol { var min: usize = 0; var max: usize = symbols.len - 1; @@ -785,7 +670,7 @@ test "machoSearchSymbols" { try testing.expectEqual(&symbols[2], machoSearchSymbols(&symbols, 5000).?); } -fn printUnknownSource(debug_info: *DebugInfo, out_stream: anytype, address: usize, tty_config: TTY.Config) !void { +fn printUnknownSource(debug_info: *DebugInfo, out_stream: anytype, address: usize, tty_config: io.tty.Config) !void { const module_name = debug_info.getModuleNameForAddress(address); return printLineInfo( out_stream, @@ -798,7 +683,7 @@ fn printUnknownSource(debug_info: *DebugInfo, out_stream: anytype, address: usiz ); } -pub fn printSourceAtAddress(debug_info: *DebugInfo, out_stream: anytype, address: usize, tty_config: TTY.Config) !void { +pub fn printSourceAtAddress(debug_info: *DebugInfo, out_stream: anytype, address: usize, tty_config: io.tty.Config) !void { const module = debug_info.getModuleForAddress(address) catch |err| switch (err) { error.MissingDebugInfo, error.InvalidDebugInfo => return printUnknownSource(debug_info, out_stream, address, tty_config), else => return err, @@ -827,7 +712,7 @@ fn printLineInfo( address: usize, symbol_name: []const u8, compile_unit_name: []const u8, - tty_config: TTY.Config, + tty_config: io.tty.Config, comptime printLineFromFile: anytype, ) !void { nosuspend { @@ -2193,7 +2078,7 @@ test "manage resources correctly" { const writer = std.io.null_writer; var di = try openSelfDebugInfo(testing.allocator); defer di.deinit(); - try printSourceAtAddress(&di, writer, showMyTrace(), detectTTYConfig(std.io.getStdErr())); + try printSourceAtAddress(&di, writer, showMyTrace(), io.tty.detectConfig(std.io.getStdErr())); } noinline fn showMyTrace() usize { @@ -2253,7 +2138,7 @@ pub fn ConfigurableTrace(comptime size: usize, comptime stack_frame_count: usize pub fn dump(t: @This()) void { if (!enabled) return; - const tty_config = detectTTYConfig(std.io.getStdErr()); + const tty_config = io.tty.detectConfig(std.io.getStdErr()); const stderr = io.getStdErr().writer(); const end = @min(t.index, size); const debug_info = getSelfDebugInfo() catch |err| { diff --git a/lib/std/io.zig b/lib/std/io.zig index d95997f853..f6d893c7dd 100644 --- a/lib/std/io.zig +++ b/lib/std/io.zig @@ -155,6 +155,8 @@ pub const BufferedAtomicFile = @import("io/buffered_atomic_file.zig").BufferedAt pub const StreamSource = @import("io/stream_source.zig").StreamSource; +pub const tty = @import("io/tty.zig"); + /// A Writer that doesn't write to anything. pub const null_writer = @as(NullWriter, .{ .context = {} }); diff --git a/lib/std/io/tty.zig b/lib/std/io/tty.zig new file mode 100644 index 0000000000..ea1c52db00 --- /dev/null +++ b/lib/std/io/tty.zig @@ -0,0 +1,121 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const File = std.fs.File; +const process = std.process; +const windows = std.os.windows; +const native_os = builtin.os.tag; + +/// Detect suitable TTY configuration options for the given file (commonly stdout/stderr). +/// This includes feature checks for ANSI escape codes and the Windows console API, as well as +/// respecting the `NO_COLOR` environment variable. +pub fn detectConfig(file: File) Config { + if (builtin.os.tag == .wasi) { + // Per https://github.com/WebAssembly/WASI/issues/162 ANSI codes + // aren't currently supported. + return .no_color; + } else if (process.hasEnvVarConstant("ZIG_DEBUG_COLOR")) { + return .escape_codes; + } else if (process.hasEnvVarConstant("NO_COLOR")) { + return .no_color; + } else if (file.supportsAnsiEscapeCodes()) { + return .escape_codes; + } else if (native_os == .windows and file.isTty()) { + var info: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined; + if (windows.kernel32.GetConsoleScreenBufferInfo(file.handle, &info) != windows.TRUE) { + // TODO: Should this return an error instead? + return .no_color; + } + return .{ .windows_api = .{ + .handle = file.handle, + .reset_attributes = info.wAttributes, + } }; + } + return .no_color; +} + +pub const Color = enum { + red, + green, + yellow, + cyan, + white, + dim, + bold, + reset, +}; + +/// Provides simple functionality for manipulating the terminal in some way, +/// such as coloring text, etc. +pub const Config = union(enum) { + no_color, + escape_codes, + windows_api: if (native_os == .windows) WindowsContext else void, + + pub const WindowsContext = struct { + handle: File.Handle, + reset_attributes: u16, + }; + + pub fn setColor(conf: Config, out_stream: anytype, color: Color) !void { + nosuspend switch (conf) { + .no_color => return, + .escape_codes => { + const color_string = switch (color) { + .red => "\x1b[31;1m", + .green => "\x1b[32;1m", + .yellow => "\x1b[33;1m", + .cyan => "\x1b[36;1m", + .white => "\x1b[37;1m", + .bold => "\x1b[1m", + .dim => "\x1b[2m", + .reset => "\x1b[0m", + }; + try out_stream.writeAll(color_string); + }, + .windows_api => |ctx| if (native_os == .windows) { + const attributes = switch (color) { + .red => windows.FOREGROUND_RED | windows.FOREGROUND_INTENSITY, + .green => windows.FOREGROUND_GREEN | windows.FOREGROUND_INTENSITY, + .yellow => windows.FOREGROUND_RED | windows.FOREGROUND_GREEN | windows.FOREGROUND_INTENSITY, + .cyan => windows.FOREGROUND_GREEN | windows.FOREGROUND_BLUE | windows.FOREGROUND_INTENSITY, + .white, .bold => windows.FOREGROUND_RED | windows.FOREGROUND_GREEN | windows.FOREGROUND_BLUE | windows.FOREGROUND_INTENSITY, + .dim => windows.FOREGROUND_INTENSITY, + .reset => ctx.reset_attributes, + }; + try windows.SetConsoleTextAttribute(ctx.handle, attributes); + } else { + unreachable; + }, + }; + } + + pub fn writeDEC(conf: Config, writer: anytype, codepoint: u8) !void { + const bytes = switch (conf) { + .no_color, .windows_api => switch (codepoint) { + 0x50...0x5e => @as(*const [1]u8, &codepoint), + 0x6a => "+", // ┘ + 0x6b => "+", // ┐ + 0x6c => "+", // ┌ + 0x6d => "+", // └ + 0x6e => "+", // ┼ + 0x71 => "-", // ─ + 0x74 => "+", // ├ + 0x75 => "+", // ┤ + 0x76 => "+", // ┴ + 0x77 => "+", // ┬ + 0x78 => "|", // │ + else => " ", // TODO + }, + .escape_codes => switch (codepoint) { + // Here we avoid writing the DEC beginning sequence and + // ending sequence in separate syscalls by putting the + // beginning and ending sequence into the same string + // literals, to prevent terminals ending up in bad states + // in case a crash happens between syscalls. + inline 0x50...0x7f => |x| "\x1B\x28\x30" ++ [1]u8{x} ++ "\x1B\x28\x42", + else => unreachable, + }, + }; + return writer.writeAll(bytes); + } +}; diff --git a/lib/std/testing.zig b/lib/std/testing.zig index 8576ec0c83..7986c50eaf 100644 --- a/lib/std/testing.zig +++ b/lib/std/testing.zig @@ -279,7 +279,7 @@ test "expectApproxEqRel" { /// This function is intended to be used only in tests. When the two slices are not /// equal, prints diagnostics to stderr to show exactly how they are not equal (with /// the differences highlighted in red), then returns a test failure error. -/// The colorized output is optional and controlled by the return of `std.debug.detectTTYConfig()`. +/// The colorized output is optional and controlled by the return of `std.io.tty.detectConfig()`. /// If your inputs are UTF-8 encoded strings, consider calling `expectEqualStrings` instead. pub fn expectEqualSlices(comptime T: type, expected: []const T, actual: []const T) !void { if (expected.ptr == actual.ptr and expected.len == actual.len) { @@ -312,7 +312,7 @@ pub fn expectEqualSlices(comptime T: type, expected: []const T, actual: []const const actual_window = actual[window_start..@min(actual.len, window_start + max_window_size)]; const actual_truncated = window_start + actual_window.len < actual.len; - const ttyconf = std.debug.detectTTYConfig(std.io.getStdErr()); + const ttyconf = std.io.tty.detectConfig(std.io.getStdErr()); var differ = if (T == u8) BytesDiffer{ .expected = expected_window, .actual = actual_window, @@ -379,7 +379,7 @@ fn SliceDiffer(comptime T: type) type { start_index: usize, expected: []const T, actual: []const T, - ttyconf: std.debug.TTY.Config, + ttyconf: std.io.tty.Config, const Self = @This(); @@ -398,7 +398,7 @@ fn SliceDiffer(comptime T: type) type { const BytesDiffer = struct { expected: []const u8, actual: []const u8, - ttyconf: std.debug.TTY.Config, + ttyconf: std.io.tty.Config, pub fn write(self: BytesDiffer, writer: anytype) !void { var expected_iterator = ChunkIterator{ .bytes = self.expected }; diff --git a/lib/std/zig/ErrorBundle.zig b/lib/std/zig/ErrorBundle.zig index f74d82273a..46b5799807 100644 --- a/lib/std/zig/ErrorBundle.zig +++ b/lib/std/zig/ErrorBundle.zig @@ -148,7 +148,7 @@ pub fn nullTerminatedString(eb: ErrorBundle, index: usize) [:0]const u8 { } pub const RenderOptions = struct { - ttyconf: std.debug.TTY.Config, + ttyconf: std.io.tty.Config, include_reference_trace: bool = true, include_source_line: bool = true, include_log_text: bool = true, @@ -181,7 +181,7 @@ fn renderErrorMessageToWriter( err_msg_index: MessageIndex, stderr: anytype, kind: []const u8, - color: std.debug.TTY.Color, + color: std.io.tty.Color, indent: usize, ) anyerror!void { const ttyconf = options.ttyconf; diff --git a/src/main.zig b/src/main.zig index 650741e5e4..afda88cebd 100644 --- a/src/main.zig +++ b/src/main.zig @@ -6044,9 +6044,9 @@ const ClangSearchSanitizer = struct { }; }; -fn get_tty_conf(color: Color) std.debug.TTY.Config { +fn get_tty_conf(color: Color) std.io.tty.Config { return switch (color) { - .auto => std.debug.detectTTYConfig(std.io.getStdErr()), + .auto => std.io.tty.detectConfig(std.io.getStdErr()), .on => .escape_codes, .off => .no_color, }; diff --git a/test/src/Cases.zig b/test/src/Cases.zig index 63dd2fd3da..08568d0dd6 100644 --- a/test/src/Cases.zig +++ b/test/src/Cases.zig @@ -1354,7 +1354,7 @@ fn runOneCase( defer all_errors.deinit(allocator); if (all_errors.errorMessageCount() > 0) { all_errors.renderToStdErr(.{ - .ttyconf = std.debug.detectTTYConfig(std.io.getStdErr()), + .ttyconf = std.io.tty.detectConfig(std.io.getStdErr()), }); // TODO print generated C code return error.UnexpectedCompileErrors;