1/// Abstraction for writing to a stream that might support terminal escape
2/// codes.
3const Terminal = @This();
4
5const builtin = @import("builtin");
6const is_windows = builtin.os.tag == .windows;
7
8const std = @import("std");
9const Io = std.Io;
10const File = std.Io.File;
11
12writer: *Io.Writer,
13mode: Mode,
14
15pub const Color = enum {
16 black,
17 red,
18 green,
19 yellow,
20 blue,
21 magenta,
22 cyan,
23 white,
24 bright_black,
25 bright_red,
26 bright_green,
27 bright_yellow,
28 bright_blue,
29 bright_magenta,
30 bright_cyan,
31 bright_white,
32 dim,
33 bold,
34 reset,
35};
36
37pub const Mode = union(enum) {
38 no_color,
39 escape_codes,
40 windows_api: WindowsApi,
41
42 pub const WindowsApi = if (!is_windows) noreturn else struct {
43 io: Io,
44 file: File,
45 reset_attributes: u16,
46 };
47
48 /// Detect suitable TTY configuration options for the given file (commonly
49 /// stdout/stderr).
50 ///
51 /// Will attempt to enable ANSI escape code support if necessary/possible.
52 ///
53 /// * `NO_COLOR` indicates whether "NO_COLOR" environment variable is
54 /// present and non-empty.
55 /// * `CLICOLOR_FORCE` indicates whether "CLICOLOR_FORCE" environment
56 /// variable is present and non-empty.
57 pub fn detect(io: Io, file: File, NO_COLOR: bool, CLICOLOR_FORCE: bool) Io.Cancelable!Mode {
58 const force_color: ?bool = if (NO_COLOR) false else if (CLICOLOR_FORCE) true else null;
59 if (force_color == false) return .no_color;
60
61 if (file.enableAnsiEscapeCodes(io)) |_| {
62 return .escape_codes;
63 } else |err| switch (err) {
64 error.Canceled => |e| return e,
65 error.NotTerminalDevice, error.Unexpected => {},
66 }
67
68 if (is_windows and try file.isTty(io)) {
69 var get_console_info = std.os.windows.CONSOLE.USER_IO.GET_SCREEN_BUFFER_INFO;
70 switch (try get_console_info.operate(io, file)) {
71 .SUCCESS => return .{ .windows_api = .{
72 .io = io,
73 .file = file,
74 .reset_attributes = get_console_info.Data.wAttributes,
75 } },
76 else => {},
77 }
78 }
79 return if (force_color == true) .escape_codes else .no_color;
80 }
81};
82
83pub const SetColorError = Io.Cancelable || Io.UnexpectedError || Io.Writer.Error;
84
85pub fn setColor(t: Terminal, color: Color) SetColorError!void {
86 switch (t.mode) {
87 .no_color => return,
88 .escape_codes => {
89 const color_string = switch (color) {
90 .black => "\x1b[30m",
91 .red => "\x1b[31m",
92 .green => "\x1b[32m",
93 .yellow => "\x1b[33m",
94 .blue => "\x1b[34m",
95 .magenta => "\x1b[35m",
96 .cyan => "\x1b[36m",
97 .white => "\x1b[37m",
98 .bright_black => "\x1b[90m",
99 .bright_red => "\x1b[91m",
100 .bright_green => "\x1b[92m",
101 .bright_yellow => "\x1b[93m",
102 .bright_blue => "\x1b[94m",
103 .bright_magenta => "\x1b[95m",
104 .bright_cyan => "\x1b[96m",
105 .bright_white => "\x1b[97m",
106 .bold => "\x1b[1m",
107 .dim => "\x1b[2m",
108 .reset => "\x1b[0m",
109 };
110 try t.writer.writeAll(color_string);
111 },
112 .windows_api => |wa| {
113 const windows = std.os.windows;
114 const attributes: windows.WORD = switch (color) {
115 .black => 0,
116 .red => windows.FOREGROUND_RED,
117 .green => windows.FOREGROUND_GREEN,
118 .yellow => windows.FOREGROUND_RED | windows.FOREGROUND_GREEN,
119 .blue => windows.FOREGROUND_BLUE,
120 .magenta => windows.FOREGROUND_RED | windows.FOREGROUND_BLUE,
121 .cyan => windows.FOREGROUND_GREEN | windows.FOREGROUND_BLUE,
122 .white => windows.FOREGROUND_RED | windows.FOREGROUND_GREEN | windows.FOREGROUND_BLUE,
123 .bright_black => windows.FOREGROUND_INTENSITY,
124 .bright_red => windows.FOREGROUND_RED | windows.FOREGROUND_INTENSITY,
125 .bright_green => windows.FOREGROUND_GREEN | windows.FOREGROUND_INTENSITY,
126 .bright_yellow => windows.FOREGROUND_RED | windows.FOREGROUND_GREEN | windows.FOREGROUND_INTENSITY,
127 .bright_blue => windows.FOREGROUND_BLUE | windows.FOREGROUND_INTENSITY,
128 .bright_magenta => windows.FOREGROUND_RED | windows.FOREGROUND_BLUE | windows.FOREGROUND_INTENSITY,
129 .bright_cyan => windows.FOREGROUND_GREEN | windows.FOREGROUND_BLUE | windows.FOREGROUND_INTENSITY,
130 .bright_white, .bold => windows.FOREGROUND_RED | windows.FOREGROUND_GREEN | windows.FOREGROUND_BLUE | windows.FOREGROUND_INTENSITY,
131 // "dim" is not supported using basic character attributes, but let's still make it do *something*.
132 // This matches the old behavior of TTY.Color before the bright variants were added.
133 .dim => windows.FOREGROUND_INTENSITY,
134 .reset => wa.reset_attributes,
135 };
136 try t.writer.flush();
137 var set_text_attribute = windows.CONSOLE.USER_IO.SET_TEXT_ATTRIBUTE(attributes);
138 switch (try set_text_attribute.operate(wa.io, wa.file)) {
139 .SUCCESS => {},
140 else => |status| return windows.unexpectedStatus(status),
141 }
142 },
143 }
144}