mirror of
https://github.com/carbon-language/carbon-lang.git
synced 2026-09-24 15:10:12 +01:00
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
This commit is contained in:
+36
-10
@@ -44,11 +44,38 @@ static constexpr std::array<char32_t, 16> 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<char32_t, 16> 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 <typename PlaceFn>
|
||||
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;
|
||||
|
||||
+45
-36
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user