From 40aa4419c03f1ed0f2333d775ee1780660b08010 Mon Sep 17 00:00:00 2001 From: Chandler Carruth Date: Fri, 21 Aug 2026 22:19:12 +0000 Subject: [PATCH] Update the terminal library for rendering diagnostics (#7659) Everything drawn into a buffer was checked against `columns()`. That is right for wrapping and for line drawing, both of which have somewhere else to put what doesn't fit, but wrong for `DrawText`, which exists for text that must not be broken and sometimes has to run past the width with no other answer available. It and `DrawCodePoint` now check only that the column is non-negative and the row is one a grid can index, and widen the buffer as far as the text needs; `DrawWrappedText`, `DrawHorizontalLine`, `DrawVerticalLine`, and `DrawBox` are unchanged. That also settles what a caller does after a drawing overhangs, since `DrawEnd` exists so that a run can continue where the last one ended, and that continuation was itself a checked error whenever the previous run overhung. A column computed to be negative, such as a gutter narrower than the line number it holds, still fails. A color picked to read against black is hard to read against white, and nothing in `Capabilities` said which a stream was going into. `ChooseBackground` reads `COLORFGBG`, which `rxvt` and its derivatives set to the foreground and background palette indices, and takes anything it doesn't answer to be dark: guessing dark costs contrast, while guessing light puts pale text on a pale background. Asking the terminal itself with an `OSC 11` query is the accurate answer, and needs raw mode, a timeout, and somewhere to put the reply, so there is a TODO for it rather than an implementation. Every corner, tee, and crossing came out of `Charset::Ascii` as `+`, which left six of the shapes a diagnostic draws indistinguishable: the rule closing a frame read as the one separating two snippets, and the anchor opening a diagnostic as the one carrying it on. Each stand-in now keeps the axis its line runs through, which leaves `+` meaning a crossing and nothing else. A tee keeps its through-stroke and leaves the branch to what is drawn beside it, and a corner is `.` where its line leaves downward and `'` where it arrives from above, which is where those characters sit in their cells. A box is a box again: ``` +--+ .--. | | -> | | +--+ '--' ``` Assisted-by: Claude Code --- common/terminal/buffer.cpp | 46 +++++++++++---- common/terminal/buffer.h | 81 +++++++++++++++------------ common/terminal/buffer_test.cpp | 56 +++++++++++++----- common/terminal/capabilities.cpp | 27 +++++++++ common/terminal/capabilities.h | 50 +++++++++++++++++ common/terminal/capabilities_test.cpp | 53 ++++++++++++++++++ 6 files changed, 254 insertions(+), 59 deletions(-) diff --git a/common/terminal/buffer.cpp b/common/terminal/buffer.cpp index 2cb0c5998f80..8ee22991236e 100644 --- a/common/terminal/buffer.cpp +++ b/common/terminal/buffer.cpp @@ -44,11 +44,38 @@ static constexpr std::array Utf8LineGlyphs = { U'┼', // left, right, up, down }; -// The ASCII stand-ins, which can only distinguish horizontal, vertical, and -// everything else. +// The ASCII stand-ins. Each keeps the axis its line runs through, which leaves +// `+` meaning a crossing and nothing else: +// +// - Running through horizontally is `-`, vertically `|`, and both ways `+`. +// - A tee keeps its through-stroke and leaves the branch to what is drawn +// beside it: the dashes either side of a `|` are what `├` and `┤` reach, and +// the line under a `-` is what a `┬` reaches. Drawing a tee as `+` reads as +// the crossing it is not. +// - A corner is `.` where its line leaves downward and `'` where it arrives +// from above, which is where those characters sit in their cells. +// - A point, a line between one center and itself, is `.`. +// +// What a diagnostic draws is then still told apart: the rule closing a snippet +// from the one separating two, and the anchor opening a diagnostic from the one +// carrying it on. static constexpr std::array AsciiLineGlyphs = { - U'+', U'-', U'-', U'-', U'|', U'+', U'+', U'+', - U'|', U'+', U'+', U'+', U'|', U'+', U'+', U'+', + U'.', // (none): a point + U'-', // left + U'-', // right + U'-', // left, right + U'|', // up + U'\'', // left, up + U'\'', // right, up + U'-', // left, right, up + U'|', // down + U'.', // left, down + U'.', // right, down + U'-', // left, right, down + U'|', // up, down + U'|', // left, up, down + U'|', // right, up, down + U'+', // left, right, up, down }; // Returns the next tab stop after `x` on a line whose stops are `tab_width` @@ -170,7 +197,7 @@ auto Buffer::AttachCombiningMark(int x, int y, char32_t code_point) -> void { auto Buffer::DrawCodePoint(int x, int y, char32_t code_point, const Style& style) -> DrawEnd { - CheckOrigin(x, y); + CheckTextOrigin(x, y); return {.x = PlaceCodePoint(x, y, code_point, style), .y = y}; } @@ -299,11 +326,10 @@ template auto Buffer::WalkText(int x, int y, int margin, llvm::StringRef text, PlaceFn place) const -> DrawEnd { CheckTextSize(text); - CARBON_CHECK( - margin >= 0 && margin <= x && x < columns_ && y >= 0 && y < MaxRows, - "Text at ({0}, {1}) with a margin of {2} is outside the {3} " - "columns and {4} rows a buffer covers, or left of its margin.", - x, y, margin, columns_, MaxRows); + CARBON_CHECK(margin >= 0 && margin <= x && y >= 0 && y < MaxRows, + "Text at ({0}, {1}) with a margin of {2} is outside the {3} " + "rows a buffer covers, or left of its margin.", + x, y, margin, MaxRows); int cur_x = x; int cur_y = y; diff --git a/common/terminal/buffer.h b/common/terminal/buffer.h index 31495ec17fa7..b62170292cf8 100644 --- a/common/terminal/buffer.h +++ b/common/terminal/buffer.h @@ -101,21 +101,20 @@ enum class LineEnd : int8_t { // up to `MaxRows` -- so laying out is a question of how many rows something // takes, never of how wide the grid will turn out to be. // -// Coordinates are the caller's to get right. Drawing a line outside the width, -// or starting text outside it, is a programming error and is checked: a caller -// deciding where to put something already knows the width, since it is what -// decided the layout, and a drawing that lands outside it is a bug in that -// layout rather than something to silently clip. Origins are checked against -// `MaxRows` the same way, though text that runs off the bottom on its own -// newlines is clipped rather than checked, as an overhang is. +// The two ways of drawing text differ in whether what they draw is held to the +// width. `DrawText` does not wrap, so text it is given has nowhere else to go: +// it widens the buffer, and `width()` grows past `columns()`. +// `DrawWrappedText` and line drawing are held to the width, since wrapping has +// the next row and a line running outside it came from a wrong extent. A +// drawing of either that starts or ends outside the width is a programming +// error and is checked; a caller placing one already knows the width, since it +// is what decided the layout. // -// A row can still end up wider than `columns()`. Text that starts inside the -// width may run off the right of it: a quoted source line longer than the room -// left, a double-width character in the last column, and above all a word -// wrapping cannot break, which is moved to a row of its own and then overhangs -// it. Breaking that word is the alternative, and it costs a reader the ability -// to copy or click it. So `width()` can exceed `columns()`, while nothing is -// ever drawn left of the origin or beyond `MaxColumns`. +// A wrapped block widens the buffer only by the words in it, never by where it +// was told to start: a word it cannot break overhangs, for the reason above. +// +// Nothing is drawn left of the origin or past `MaxColumns` either way, and text +// running off the bottom on its own newlines is clipped rather than checked. // // A combining mark renders into the cell before it, so one with no cell before // it -- at column zero, or on a row nothing has been drawn on -- has nowhere to @@ -138,11 +137,11 @@ class Buffer { // The bounds a buffer exists within. // // These are far past anything a terminal displays, and exist so that a cell - // index stays representable rather than to ration anything. `columns()` and - // every row drawn into are checked against them, so a caller cannot reach - // outside them by asking. What can reach `MaxColumns` without being asked for - // is a word overhanging the target width, and that alone is clipped rather - // than checked, since how far it overhangs is a fact about the text. + // index stays representable rather than to ration anything. Unlike + // `columns()`, every way of drawing is held to them: past them nothing is + // drawn and the column still advances, so measuring and drawing agree. + // Clipped rather than checked, since how far unwrapped text or an overhang + // runs is a fact about the text. static constexpr int MaxColumns = 1 << 14; static constexpr int MaxRows = 1 << 16; @@ -207,8 +206,9 @@ class Buffer { // Returns the width everything drawn into the buffer is laid out for. auto columns() const -> int { return columns_; } - // Returns the columns the grid currently holds: `columns()` until something - // overhangs it, and at least enough to hold the overhang after that. + // Returns the columns the grid currently holds: `columns()` until unwrapped + // text or an overhanging word reached past it, and at least enough to hold + // what did after that. auto width() const -> int { return width_; } // Returns the number of rows the grid holds, which is one past the last row @@ -256,15 +256,14 @@ class Buffer { // So this is a layout preference rather than a minimum. auto MeasureWrapWidth(llvm::StringRef text) const -> int; - // Draws `code_point` at (x, y), which must be inside `columns()` and - // `MaxRows`, adding rows as needed to reach it. + // Draws `code_point` at (x, y), which must be a non-negative column and a row + // inside `MaxRows`, widening the buffer and adding rows as needed to reach + // it. One code point is unwrapped text, so it is not held to the width. // // Returns the column after it, which is `x` again for a combining mark since - // one renders into the column before it. A double-width character starting in - // the last column is drawn rather than refused, and takes the column after - // it: half a character is not something a terminal can render, so the choice - // is between the whole of it and none, and this is the same overhang wrapping - // allows a word that fits no row. + // one renders into the column before it. A double-width character takes both + // its columns wherever it starts: half a character is not something a + // terminal can render, so the choice is between the whole of it and none. auto DrawCodePoint(int x, int y, char32_t code_point, const Style& style) -> DrawEnd; @@ -307,14 +306,15 @@ class Buffer { auto DrawBox(int x, int y, int box_width, int box_height, const Style& style) -> DrawEnd; - // Draws `text` starting at (x, y), which must be inside `columns()`, as part - // of text whose left edge is `margin`. + // Draws `text` starting at (x, y), which must be a column at or right of + // `margin` and a row inside `MaxRows`, as part of text whose left edge is + // `margin`. // - // Nothing here wraps, so text with no newline in it runs off the right of the - // width when it is longer than the room left, exactly as an overhanging word - // does. That is what this is for: a source line is quoted as it was written, - // and deciding how much of one to show is the caller's, made against - // `columns()` before the quoting starts. + // Nothing here wraps, so text runs off the right of the width when it is + // longer than the room left, and the buffer widens to hold it. That is what + // this is for: text that must not be broken, such as a source line quoted as + // it was written. A caller that wants the text held to the width wants + // `DrawWrappedText`. // // Newlines return to column `margin` on the next row, carriage returns to // column `margin` on the same row, and tabs advance to the next tab stop, @@ -462,11 +462,20 @@ class Buffer { return cells_[CellIndex(x, y)]; } - // Checks that (x, y) is somewhere a drawing may start. + // Checks that (x, y) is somewhere unwrapped text may start, which the width + // does not decide. // // The text walks check this themselves, together with the bounds particular // to each: they are inlined into every text operation, and one check there // costs measurably less than two. + auto CheckTextOrigin(int x, int y) const -> void { + CARBON_CHECK(x >= 0 && y >= 0 && y < MaxRows, + "Drawing text at ({0}, {1}) is outside the {2} rows a buffer " + "covers.", + x, y, MaxRows); + } + + // Checks that (x, y) is somewhere a drawing held to the width may start. auto CheckOrigin(int x, int y) const -> void { CARBON_CHECK( x >= 0 && x < columns_ && y >= 0 && y < MaxRows, diff --git a/common/terminal/buffer_test.cpp b/common/terminal/buffer_test.cpp index 822ce8f5d798..b60e0f5b8dbf 100644 --- a/common/terminal/buffer_test.cpp +++ b/common/terminal/buffer_test.cpp @@ -170,6 +170,19 @@ TEST(BufferTest, Tees) { "├─┼─┤\n" "│ │ │\n" "╰─┴─╯\n"); + + // The same table in ASCII, where every tee keeps its through-stroke and only + // the crossing in the middle is a `+`. + Buffer ascii(5, Charset::Ascii); + ascii.DrawBox(0, 0, 5, 5, Style()); + ascii.DrawHorizontalLine(0, 2, 5, Style()); + ascii.DrawVerticalLine(2, 0, 5, Style()); + EXPECT_EQ(Render(ascii), + ".---.\n" + "| | |\n" + "|-+-|\n" + "| | |\n" + "'---'\n"); } TEST(BufferTest, ALineBetweenOneCenterAndItselfIsAPoint) { @@ -189,10 +202,11 @@ TEST(BufferTest, ALineBetweenOneCenterAndItselfIsAPoint) { joined.DrawHorizontalLine(0, 0, 3, Style()); EXPECT_EQ(Render(joined), "╶─╴\n"); - // ASCII has one glyph for everything that isn't a plain segment. + // A point is a small mark in either character set, rather than the junction + // ASCII draws where lines really cross. Buffer ascii(3, Charset::Ascii); ascii.DrawVerticalLine(1, 0, 1, Style()); - EXPECT_EQ(Render(ascii), " +\n"); + EXPECT_EQ(Render(ascii), " .\n"); // A line with no length draws nothing at all. Buffer empty(3, Charset::Utf8); @@ -296,13 +310,15 @@ TEST(BufferTest, Box) { "│ │\n" "╰──╯\n"); - // ASCII can only tell horizontal and vertical apart from everything else. + // The ASCII stand-ins keep the shape: the sides run and the corners turn, + // with the character that sits low where the line leaves downward and the one + // that sits high where it arrives from above. Buffer ascii(4, Charset::Ascii); ascii.DrawBox(0, 0, 4, 3, Style()); EXPECT_EQ(Render(ascii), - "+--+\n" + ".--.\n" "| |\n" - "+--+\n"); + "'--'\n"); // A box with no interior is the single line that bounds it. Buffer flat(4, Charset::Utf8); @@ -846,7 +862,8 @@ TEST(BufferTest, MeasureWrapWidth) { EXPECT_EQ(buffer.MeasureWrapWidth("a bb ccc"), 3); EXPECT_EQ(buffer.MeasureWrapWidth(" spaced out "), 6); - // Newlines and tabs bound a word without taking columns of their own. + // A word ends at a newline or a tab, neither of which takes columns of its + // own. EXPECT_EQ(buffer.MeasureWrapWidth("a\nbb\tccc"), 3); // A word is measured in the columns it takes, not the bytes it holds. @@ -990,24 +1007,37 @@ TEST(BufferDeathTest, WidthMustFitTheGrid) { "Buffer width must be in"); } -TEST(BufferDeathTest, DrawingMustStartInsideTheGrid) { - // A caller placing something already knows the width, since it is what - // decided the layout, so landing outside it is a bug in that layout rather - // than something to quietly drop. Rows are checked the same way. +TEST(BufferDeathTest, TextMustStartInsideTheGrid) { + // Unwrapped text is not held to the width, so only a column before the + // origin or a row no grid can index is a mistake. Buffer buffer(10, Charset::Ascii); - EXPECT_DEATH(buffer.DrawCodePoint(10, 0, 'a', Style()), "is outside the"); EXPECT_DEATH(buffer.DrawCodePoint(-1, 0, 'a', Style()), "is outside the"); EXPECT_DEATH(buffer.DrawCodePoint(0, -1, 'a', Style()), "is outside the"); EXPECT_DEATH(buffer.DrawCodePoint(0, Buffer::MaxRows, 'a', Style()), "is outside the"); - EXPECT_DEATH(buffer.DrawText(10, 0, "a", Style()), "is outside the"); - EXPECT_DEATH(buffer.MeasureText(10, 0, "a"), "is outside the"); + EXPECT_DEATH(buffer.DrawText(-1, 0, "a", Style()), "is outside the"); + EXPECT_DEATH(buffer.MeasureText(0, Buffer::MaxRows, "a"), "is outside the"); // Text begins at or right of the margin its rows return to. EXPECT_DEATH(buffer.DrawText(2, 0, 3, "a", Style()), "left of its margin"); EXPECT_DEATH(buffer.MeasureText(2, 0, -1, "a"), "left of its margin"); } +TEST(BufferTest, UnwrappedTextWidensTheBuffer) { + // `DrawText` widens the buffer rather than being held to its width, so a run + // continues from where the one before it ended however far past that is. + Buffer buffer(10, Charset::Ascii); + Buffer::DrawEnd end = + buffer.DrawText(0, 0, "a message far longer than ten columns", Style()); + EXPECT_EQ(end, DrawEnd(37, 0)); + EXPECT_EQ(buffer.columns(), 10); + EXPECT_GE(buffer.width(), 37); + + // Which is what lets a row be built from runs that carry different styles. + buffer.DrawText(end.x, end.y, " [tag]", Style().Bold()); + EXPECT_EQ(Render(buffer), "a message far longer than ten columns [tag]\n"); +} + TEST(BufferDeathTest, LinesMustFitWhatTheyAreDrawnInto) { // Nothing about a line is unbreakable, so unlike text it has no reason to // reach outside the width, and one that does came from a wrong extent. diff --git a/common/terminal/capabilities.cpp b/common/terminal/capabilities.cpp index 20e4fe719264..98a35b7c27e4 100644 --- a/common/terminal/capabilities.cpp +++ b/common/terminal/capabilities.cpp @@ -169,6 +169,31 @@ auto ChooseCharset(Preference preference, llvm::StringRef locale) -> Charset { : Charset::Ascii; } +auto ChooseBackground(BackgroundPreference preference, + llvm::StringRef colorfgbg) -> Background { + switch (preference) { + case BackgroundPreference::Dark: + return Background::Dark; + case BackgroundPreference::Light: + return Background::Light; + case BackgroundPreference::Auto: + break; + } + + // The background is the last field, since some terminals write a third one + // between the foreground and it. + llvm::StringRef background = colorfgbg.rsplit(';').second; + unsigned index = 0; + if (!llvm::to_integer(background, index) || index > 15) { + // Anything else, including the `default` some terminals write and the + // variable being unset, says nothing. + return Background::Dark; + } + // The first eight palette entries are the dark half, except that the eighth + // is white and the ninth is the dark gray that follows it. + return (index <= 6 || index == 8) ? Background::Dark : Background::Light; +} + // Returns the locale that determines the terminal's character encoding, // following the precedence POSIX defines for `LC_CTYPE`. static auto GetLocale() -> llvm::StringRef { @@ -208,6 +233,8 @@ auto Capabilities::Detect(Filesystem::WriteFileRef file, ChooseColorMode(preferences.color, ColorEnvironment::FromProcess(), capabilities.is_terminal); capabilities.charset = ChooseCharset(preferences.utf8, GetLocale()); + capabilities.background = + ChooseBackground(preferences.background, GetEnv("COLORFGBG")); capabilities.columns = GetColumns(fd); return capabilities; diff --git a/common/terminal/capabilities.h b/common/terminal/capabilities.h index 51bac57f7367..109b5f3b019b 100644 --- a/common/terminal/capabilities.h +++ b/common/terminal/capabilities.h @@ -58,11 +58,33 @@ enum class Preference : int8_t { Always, }; +// What the terminal draws its text on. +// +// Nothing about the rendering depends on the exact color, only on which side of +// the middle it sits: a color chosen to read on one is hard to read on the +// other. +enum class Background : int8_t { + Dark, + Light, +}; + +// An explicit statement of what the terminal draws its text on, where `Auto` +// leaves it to detection. +// +// This is a tri-state like `Preference`, but its two settings name the answer +// rather than turning a feature on and off, so it is an enum of its own. +enum class BackgroundPreference : int8_t { + Auto, + Dark, + Light, +}; + // An explicit preference for each feature detection decides about, normally // parsed from command line flags. struct Preferences { Preference color = Preference::Auto; Preference utf8 = Preference::Auto; + BackgroundPreference background = BackgroundPreference::Auto; }; // The environment variables that control whether and how color is used. @@ -129,6 +151,23 @@ auto ChooseColorMode(Preference preference, const ColorEnvironment& env, // the other way only makes output plainer. auto ChooseCharset(Preference preference, llvm::StringRef locale) -> Charset; +// Returns what the terminal draws its text on, where `colorfgbg` is the value +// of `COLORFGBG`. +// +// That variable is the only thing a process can read without talking to the +// terminal. `rxvt` and its derivatives set it, as do a few others, to the +// foreground and background palette indices separated by `;` -- sometimes with +// a third field between them -- so the background is the last of them. An index +// of 0 through 6 or 8 is a dark one, 7 and 9 through 15 a light one, and +// anything outside that range says nothing. +// +// It is missing far more often than it is present, and stale when the user +// changes their theme without restarting, so anything it doesn't answer is +// treated as dark. Guessing wrong that way costs contrast; guessing wrong the +// other way puts light text on a light background. +auto ChooseBackground(BackgroundPreference preference, + llvm::StringRef colorfgbg) -> Background; + // The width to lay out for when nothing says how wide the output is. // // Layout always has a width to fit, because the alternative is output laid out @@ -163,6 +202,14 @@ struct Capabilities { // are what answer the question and no stream abstraction exposes them. // LLVM's `raw_ostream::has_colors()` is not a substitute for the enablement // rule above, which recognizes terminals its `TERM` list doesn't. + // + // An `OSC 11` query would ask the terminal what it draws on, which is the + // only accurate answer and what `vim`, `delta`, and `bat` do. It isn't one a + // non-interactive tool can use: the reply has to be waited for, and drawing + // with it only when it arrives in time would leave the colors depending on + // that. Reading it also consumes whatever was typed ahead for the next shell + // command, along with the input a compile may be taking from stdin. + // `COLORFGBG` is the passive stand-in that costs none of this. static auto Detect(Filesystem::WriteFileRef file, Preferences preferences = {}) -> Capabilities; @@ -172,6 +219,9 @@ struct Capabilities { // The encoding the terminal decodes output with. Charset charset = Charset::Ascii; + // What the terminal draws its text on. + Background background = Background::Dark; + // Whether the stream is attached to a terminal at all. Note that color can // still be in use when this is false, if the environment forces it. bool is_terminal = false; diff --git a/common/terminal/capabilities_test.cpp b/common/terminal/capabilities_test.cpp index 96aa272f0bd7..06f4e8c1c224 100644 --- a/common/terminal/capabilities_test.cpp +++ b/common/terminal/capabilities_test.cpp @@ -231,12 +231,57 @@ TEST(CapabilitiesTest, Charset) { EXPECT_EQ(ChooseCharset(Preference::Always, "C"), Charset::Utf8); } +TEST(CapabilitiesTest, BackgroundFromColorFgBg) { + // `fg;bg`, which is what `rxvt` and its derivatives write. + EXPECT_EQ(ChooseBackground(BackgroundPreference::Auto, "15;0"), + Background::Dark); + EXPECT_EQ(ChooseBackground(BackgroundPreference::Auto, "0;15"), + Background::Light); + // The eighth entry is white and the ninth the dark gray after it, so the + // halves are not simply the low and high eight. + EXPECT_EQ(ChooseBackground(BackgroundPreference::Auto, "0;7"), + Background::Light); + EXPECT_EQ(ChooseBackground(BackgroundPreference::Auto, "15;8"), + Background::Dark); + EXPECT_EQ(ChooseBackground(BackgroundPreference::Auto, "15;6"), + Background::Dark); + EXPECT_EQ(ChooseBackground(BackgroundPreference::Auto, "0;9"), + Background::Light); + // Some terminals write a third field between the two. + EXPECT_EQ(ChooseBackground(BackgroundPreference::Auto, "15;default;0"), + Background::Dark); + EXPECT_EQ(ChooseBackground(BackgroundPreference::Auto, "0;default;15"), + Background::Light); +} + +TEST(CapabilitiesTest, BackgroundWithNothingToGoOn) { + // Unset, unparsable, and out of range all say nothing, and what nothing + // gets is the assumption that costs contrast rather than legibility. + EXPECT_EQ(ChooseBackground(BackgroundPreference::Auto, ""), Background::Dark); + EXPECT_EQ(ChooseBackground(BackgroundPreference::Auto, "default;default"), + Background::Dark); + EXPECT_EQ(ChooseBackground(BackgroundPreference::Auto, "0;99"), + Background::Dark); + EXPECT_EQ(ChooseBackground(BackgroundPreference::Auto, "15"), + Background::Dark); + EXPECT_EQ(ChooseBackground(BackgroundPreference::Auto, "15;"), + Background::Dark); +} + +TEST(CapabilitiesTest, BackgroundPreferenceWins) { + EXPECT_EQ(ChooseBackground(BackgroundPreference::Dark, "0;15"), + Background::Dark); + EXPECT_EQ(ChooseBackground(BackgroundPreference::Light, "15;0"), + Background::Light); +} + TEST(CapabilitiesTest, Defaults) { // The defaults describe a plain-text sink, which is what a file or a pipe // gets and what tests should use unless exercising something richer. Capabilities capabilities; EXPECT_EQ(capabilities.color_mode, ColorMode::NoColor); EXPECT_EQ(capabilities.charset, Charset::Ascii); + EXPECT_EQ(capabilities.background, Background::Dark); EXPECT_FALSE(capabilities.is_terminal); EXPECT_FALSE(capabilities.columns.has_value()); } @@ -279,6 +324,14 @@ TEST(CapabilitiesTest, Detect) { Charset::Utf8); EXPECT_EQ(Capabilities::Detect(*file, {.utf8 = Preference::Never}).charset, Charset::Ascii); + EXPECT_EQ( + Capabilities::Detect(*file, {.background = BackgroundPreference::Light}) + .background, + Background::Light); + EXPECT_EQ( + Capabilities::Detect(*file, {.background = BackgroundPreference::Dark}) + .background, + Background::Dark); (*std::move(file)).Close().Check(); }