Commit 0f6fa3f20b

Linus Groh <mail@linusgroh.de>
2023-05-21 15:27:28
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.
1 parent 39c2eee
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),
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);
+    }
+};
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;
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 {};
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)});
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| {
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 = {} });
 
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 };
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,
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,
     };
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;