Description
This is something of a follow-up to #15806.
Before that PR, all colors had an implicit 'bold,' and all colors in the Windows API had an implicit FOREGROUND_INTENSITY
.
I've pushed an update to make colors not bold by default (the
;1m
), which kind of defeated the purpose of.bold
This makes sense to do from the perspective of ANSI escape sequences, but it complicates making setColor
work similarly when the Config
is .windows_api
. In the Windows API, there's no builtin way to persist something like .bold
, so attempting to use .bold
and then set a color does not have the intended effect--the 'bold' is always lost after setting the color.
About bold
Historically, 'bold' has been implemented as making the text a brighter color rather than actually bold. From Wikipedia:
Quite a few terminals implemented "bold" (SGR code 1) as a brighter color rather than a different font, thus providing 8 additional foreground colors.
This is still an option in e.g. Gnome terminal ('Show bold text in bright colors'), and is the default behavior in Windows Terminal. This doesn't really affect the problem, but it's worth noting when thinking about potential solutions.
A visualization
This is cmd.exe
in Windows Terminal with the default font/settings:
(note: dim is not expected to match, there's no 'dim' equivalent in the Windows Console API)
Note that with modified settings to use a font that supports bold text and to render bold text as bold only (instead of the default setting of rendering bold text as 'bright colors'), the difference is less impactful visually but the difference can still be seen:
Code used to generate this output
const std = @import("std");
const is_windows = @import("builtin").os.tag == .windows;
pub fn main() !void {
const stderr = std.io.getStdErr();
if (is_windows) {
_ = std.os.windows.kernel32.SetConsoleOutputCP(65001);
}
const ansi_config = std.io.tty.Config{ .escape_codes = {} };
std.debug.print("ANSI Escape Codes:\n", .{});
try dumpColors(stderr.writer(), ansi_config);
if (is_windows) {
var info: std.os.windows.CONSOLE_SCREEN_BUFFER_INFO = undefined;
std.debug.assert(std.os.windows.kernel32.GetConsoleScreenBufferInfo(stderr.handle, &info) == std.os.windows.TRUE);
const console_config = std.io.tty.Config{ .windows_api = .{
.handle = stderr.handle,
.reset_attributes = info.wAttributes,
} };
std.debug.print("\nConsole API:\n", .{});
try dumpColors(stderr.writer(), console_config);
}
}
fn dumpColors(writer: anytype, config: std.io.tty.Config) !void {
inline for (@typeInfo(std.io.tty.Color).Enum.fields) |field| {
try config.setColor(writer, @field(std.io.tty.Color, field.name));
try writer.writeAll("■");
if (@field(std.io.tty.Color, field.name) == .black) {
try config.setColor(writer, .reset);
}
try writer.writeAll(" ");
try writer.writeAll(field.name);
try writer.writeByteNTimes(' ', 20 - field.name.len);
try config.setColor(writer, .bold);
try config.setColor(writer, @field(std.io.tty.Color, field.name));
try writer.writeAll("■");
if (@field(std.io.tty.Color, field.name) == .black) {
try config.setColor(writer, .reset);
try config.setColor(writer, .bold);
}
try writer.writeAll(" bold ");
try writer.writeAll(field.name);
try config.setColor(writer, .reset);
try writer.writeByteNTimes(' ', 20 - field.name.len);
try config.setColor(writer, .dim);
try config.setColor(writer, @field(std.io.tty.Color, field.name));
try writer.writeAll("■");
if (@field(std.io.tty.Color, field.name) == .black) {
try config.setColor(writer, .reset);
try config.setColor(writer, .dim);
}
try writer.writeAll(" dim ");
try writer.writeAll(field.name);
try config.setColor(writer, .reset);
try writer.writeAll("\n");
}
}
Potential solutions
- Go back to every-color-is-bold/every-color-is-intense. This gives us an easy path to cross-API conformity but makes the
setColor
API less useful/flexible than it could be. - Modify the
setColor
API to take a mutable*Config
, add something likeintensity: enum { normal, bold, dim }
toConfig.WindowsContext
and apply/removeFOREGROUND_INTENSITY
accordingly during eachsetColor
call. This would keep the usefulness of the API but add a wrinkle toConfig
where the same instance would need to be re-used consistently (and without otherSetConsoleTextAttribute
calls in betweensetColor
calls) in order to ensure that theintensity
state would remain correct. - Add a
GetConsoleTextAttribute
call insetColor
, check forFOREGROUND_INTENSITY
and persist it if it's present. This would provide some support for persistentbold
but it may not always behave in the intended/expected way from the perspective of the user, and would add the overhead of an extraGetConsoleTextAttribute
call. - Keep it as is and accept the incongruous behavior between the APIs. Ideally this would be supplemented by windows: detect ANSI support in more terminals #15282 getting merged which would make
.escape_codes
get used on Windows in more situations (e.g. in Windows Terminal).
I personally see some merit to all of the potential solutions, and I may be missing some potential solutions.
cc @linusg if you'd like to weigh in