mirror of
https://github.com/carbon-language/carbon-lang.git
synced 2026-09-24 13:50:10 +01:00
Implement a terminal rendering library in common/terminal (#7597)
Rich diagnostic rendering needs a layer underneath it that knows what the attached terminal can do and can position styled text in two dimensions. This adds that layer, both as the foundation the diagnostics rendering work will build on and as something usable directly for ordinary CLI output. Nothing depends on it yet, so it lands and is reviewed on its own. Four libraries, each with its own tests: - `color`: a color, either one of the 16 named ANSI colors or a 24-bit RGB value, and the escape sequences that select it at a given color depth. - `style`: colors plus text attributes, and the escapes that move a terminal from one style to another. - `capabilities`: what the terminal behind a stream supports, detected from the environment. - `buffer`: a grid of styled cells that layout code draws into and that renders itself once. Rationale for the design decisions lives in the headers, next to what it explains. Four things are worth review attention in particular: - The color detection precedence documented on `ChooseColorMode`. It settles how a `--color` flag, `NO_COLOR`, `CLICOLOR`, `FORCE_COLOR`, and the terminal itself interact. The policy is a pure function of those inputs, so the whole table is tested without touching the process environment. - `Charset`, which decides whether any UTF-8 processing happens at all. Column counts only follow from code points if the terminal agrees about the encoding, so anything short of a locale naming UTF-8 is treated as bytes. - `Buffer` owning column accounting instead of its callers, which is what keeps double-width characters, combining marks, and stray bytes from misaligning everything after them. - The API surface, which is held to operations that nothing else covers. Junctions in line art come only from lines overlapping, and turning a style on or off is spelled as a transition to or from the default style. `terminal_benchmark` covers style transitions, full-screen rendering, and text drawing. On an M-series laptop, rendering an 80x24 screen in which every cell changes style costs about 14us with color off and 76-95us with it, and drawing a 40-column line of source costs about 140ns without UTF-8 processing and 394ns with it. Assisted-by: Gemini and Claude --------- Co-authored-by: Richard Smith <richard@metafoo.co.uk>
This commit is contained in:
co-authored by
Richard Smith
parent
3bbc03f527
commit
9c486de2de
@@ -18,6 +18,7 @@ groupt
|
|||||||
indext
|
indext
|
||||||
inout
|
inout
|
||||||
isELF
|
isELF
|
||||||
|
iterm
|
||||||
parameteras
|
parameteras
|
||||||
pullrequest
|
pullrequest
|
||||||
rightt
|
rightt
|
||||||
|
|||||||
@@ -103,15 +103,8 @@ auto Internal::FileRefBase::ReadFileToString()
|
|||||||
auto Internal::FileRefBase::WriteFileFromString(llvm::StringRef str)
|
auto Internal::FileRefBase::WriteFileFromString(llvm::StringRef str)
|
||||||
-> ErrorOr<Success, FdError> {
|
-> ErrorOr<Success, FdError> {
|
||||||
CARBON_RETURN_IF_ERROR(SeekFromBeginning(0));
|
CARBON_RETURN_IF_ERROR(SeekFromBeginning(0));
|
||||||
auto bytes = llvm::ArrayRef<std::byte>(
|
CARBON_RETURN_IF_ERROR(WriteCompleteBuffer(llvm::ArrayRef<std::byte>(
|
||||||
reinterpret_cast<const std::byte*>(str.data()), str.size());
|
reinterpret_cast<const std::byte*>(str.data()), str.size())));
|
||||||
while (!bytes.empty()) {
|
|
||||||
auto write_result = WriteFromBuffer(bytes);
|
|
||||||
if (!write_result.ok()) {
|
|
||||||
return std::move(write_result).error();
|
|
||||||
}
|
|
||||||
bytes = *write_result;
|
|
||||||
}
|
|
||||||
CARBON_RETURN_IF_ERROR(Truncate(str.size()));
|
CARBON_RETURN_IF_ERROR(Truncate(str.size()));
|
||||||
return Success();
|
return Success();
|
||||||
}
|
}
|
||||||
|
|||||||
+70
-7
@@ -219,6 +219,24 @@ namespace Internal {
|
|||||||
class FileRefBase;
|
class FileRefBase;
|
||||||
} // namespace Internal
|
} // namespace Internal
|
||||||
|
|
||||||
|
// Convenience type defs for the three access combinations.
|
||||||
|
using ReadFileRef = FileRef<OpenAccess::ReadOnly>;
|
||||||
|
using WriteFileRef = FileRef<OpenAccess::WriteOnly>;
|
||||||
|
using ReadWriteFileRef = FileRef<OpenAccess::ReadWrite>;
|
||||||
|
|
||||||
|
// Returns constant references to the standard streams the process is started
|
||||||
|
// with.
|
||||||
|
//
|
||||||
|
// The returned references are non-owning: the process shares these descriptors
|
||||||
|
// with whatever started it, closing them is never correct, and unrelated code
|
||||||
|
// throughout the process may be reading or writing the same descriptor.
|
||||||
|
//
|
||||||
|
// Their descriptor numbers are fixed by the platform rather than discovered at
|
||||||
|
// runtime, so these are constant expressions.
|
||||||
|
consteval auto Stdin() -> ReadFileRef;
|
||||||
|
consteval auto Stdout() -> WriteFileRef;
|
||||||
|
consteval auto Stderr() -> WriteFileRef;
|
||||||
|
|
||||||
// Returns a constant `Dir` object that models the open current working
|
// Returns a constant `Dir` object that models the open current working
|
||||||
// directory.
|
// directory.
|
||||||
//
|
//
|
||||||
@@ -348,7 +366,13 @@ class Internal::FileRefBase {
|
|||||||
FileRefBase() = default;
|
FileRefBase() = default;
|
||||||
|
|
||||||
// Returns true if this refers to a valid open file, and false otherwise.
|
// Returns true if this refers to a valid open file, and false otherwise.
|
||||||
auto is_valid() const -> bool { return fd_ != -1; }
|
constexpr auto is_valid() const -> bool { return fd_ != -1; }
|
||||||
|
|
||||||
|
// Non-portable API only available on Unix-like systems. Returns the
|
||||||
|
// underlying file descriptor, for the platform calls this type doesn't wrap,
|
||||||
|
// such as `isatty` and `ioctl`. The descriptor remains owned by whatever owns
|
||||||
|
// this file.
|
||||||
|
constexpr auto unix_fd() const -> int { return fd_; }
|
||||||
|
|
||||||
// Reads the file status.
|
// Reads the file status.
|
||||||
//
|
//
|
||||||
@@ -405,6 +429,24 @@ class Internal::FileRefBase {
|
|||||||
auto WriteFromBuffer(llvm::ArrayRef<std::byte> buffer)
|
auto WriteFromBuffer(llvm::ArrayRef<std::byte> buffer)
|
||||||
-> ErrorOr<llvm::ArrayRef<std::byte>, FdError>;
|
-> ErrorOr<llvm::ArrayRef<std::byte>, FdError>;
|
||||||
|
|
||||||
|
// Writes the complete contents of the provided buffer.
|
||||||
|
//
|
||||||
|
// Unlike `WriteFromBuffer`, this doesn't return until every byte has been
|
||||||
|
// written or an error occurs. It repeats `WriteFromBuffer` over whatever is
|
||||||
|
// left, so each write is issued for as much of the buffer as remains and the
|
||||||
|
// whole is written in as few writes as the file allows. Anything else writing
|
||||||
|
// to the same file can only interleave between those writes, which leaves no
|
||||||
|
// room to interleave at all when the file accepts the buffer in one write.
|
||||||
|
//
|
||||||
|
// On an error, an unspecified prefix of the buffer has already been written
|
||||||
|
// and can't be un-written. How much isn't reported; a caller that needs to
|
||||||
|
// know should drive `WriteFromBuffer` itself.
|
||||||
|
//
|
||||||
|
// This method retries `EINTR` on Unix-like systems and returns other errors
|
||||||
|
// to the caller.
|
||||||
|
auto WriteCompleteBuffer(llvm::ArrayRef<std::byte> buffer)
|
||||||
|
-> ErrorOr<Success, FdError>;
|
||||||
|
|
||||||
// Returns an LLVM `raw_fd_ostream` that writes to this file.
|
// Returns an LLVM `raw_fd_ostream` that writes to this file.
|
||||||
//
|
//
|
||||||
// Note that this doesn't expose any write errors here, those will surface
|
// Note that this doesn't expose any write errors here, those will surface
|
||||||
@@ -458,7 +500,7 @@ class Internal::FileRefBase {
|
|||||||
Duration poll_interval = {}) -> ErrorOr<FileLock, FdError>;
|
Duration poll_interval = {}) -> ErrorOr<FileLock, FdError>;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
explicit FileRefBase(int fd) : fd_(fd) {}
|
explicit constexpr FileRefBase(int fd) : fd_(fd) {}
|
||||||
|
|
||||||
// Note: this should only be used or made part of the public API by subclasses
|
// Note: this should only be used or made part of the public API by subclasses
|
||||||
// that provide *ownership* of the open file. It is implemented here to
|
// that provide *ownership* of the open file. It is implemented here to
|
||||||
@@ -536,6 +578,9 @@ class FileRef : public Internal::FileRefBase {
|
|||||||
auto WriteFromBuffer(llvm::ArrayRef<std::byte> buffer)
|
auto WriteFromBuffer(llvm::ArrayRef<std::byte> buffer)
|
||||||
-> ErrorOr<llvm::ArrayRef<std::byte>, FdError>
|
-> ErrorOr<llvm::ArrayRef<std::byte>, FdError>
|
||||||
requires Writeable;
|
requires Writeable;
|
||||||
|
auto WriteCompleteBuffer(llvm::ArrayRef<std::byte> buffer)
|
||||||
|
-> ErrorOr<Success, FdError>
|
||||||
|
requires Writeable;
|
||||||
auto WriteStream() -> llvm::raw_fd_ostream
|
auto WriteStream() -> llvm::raw_fd_ostream
|
||||||
requires Writeable;
|
requires Writeable;
|
||||||
auto ReadFileToString() -> ErrorOr<std::string, FdError>
|
auto ReadFileToString() -> ErrorOr<std::string, FdError>
|
||||||
@@ -546,16 +591,14 @@ class FileRef : public Internal::FileRefBase {
|
|||||||
protected:
|
protected:
|
||||||
friend File<A>;
|
friend File<A>;
|
||||||
friend DirRef;
|
friend DirRef;
|
||||||
|
friend consteval auto Stdin() -> ReadFileRef;
|
||||||
|
friend consteval auto Stdout() -> WriteFileRef;
|
||||||
|
friend consteval auto Stderr() -> WriteFileRef;
|
||||||
|
|
||||||
// Other constructors from the base are also available, but remain protected.
|
// Other constructors from the base are also available, but remain protected.
|
||||||
using FileRefBase::FileRefBase;
|
using FileRefBase::FileRefBase;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Convenience type defs for the three access combinations.
|
|
||||||
using ReadFileRef = FileRef<OpenAccess::ReadOnly>;
|
|
||||||
using WriteFileRef = FileRef<OpenAccess::WriteOnly>;
|
|
||||||
using ReadWriteFileRef = FileRef<OpenAccess::ReadWrite>;
|
|
||||||
|
|
||||||
// An owning handle to an open file.
|
// An owning handle to an open file.
|
||||||
//
|
//
|
||||||
// This extends the `FileRef` API to provide ownership of the file handle. Most
|
// This extends the `FileRef` API to provide ownership of the file handle. Most
|
||||||
@@ -1320,6 +1363,10 @@ inline auto DurationToTimespec(Duration d) -> timespec {
|
|||||||
|
|
||||||
} // namespace Internal
|
} // namespace Internal
|
||||||
|
|
||||||
|
consteval auto Stdin() -> ReadFileRef { return ReadFileRef(STDIN_FILENO); }
|
||||||
|
consteval auto Stdout() -> WriteFileRef { return WriteFileRef(STDOUT_FILENO); }
|
||||||
|
consteval auto Stderr() -> WriteFileRef { return WriteFileRef(STDERR_FILENO); }
|
||||||
|
|
||||||
consteval auto Cwd() -> Dir { return Dir(AT_FDCWD); }
|
consteval auto Cwd() -> Dir { return Dir(AT_FDCWD); }
|
||||||
|
|
||||||
inline auto FileLock::Destroy() -> void {
|
inline auto FileLock::Destroy() -> void {
|
||||||
@@ -1431,6 +1478,14 @@ inline auto Internal::FileRefBase::WriteFromBuffer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline auto Internal::FileRefBase::WriteCompleteBuffer(
|
||||||
|
llvm::ArrayRef<std::byte> buffer) -> ErrorOr<Success, FdError> {
|
||||||
|
while (!buffer.empty()) {
|
||||||
|
CARBON_ASSIGN_OR_RETURN(buffer, WriteFromBuffer(buffer));
|
||||||
|
}
|
||||||
|
return Success();
|
||||||
|
}
|
||||||
|
|
||||||
inline auto Internal::FileRefBase::WriteStream() -> llvm::raw_fd_ostream {
|
inline auto Internal::FileRefBase::WriteStream() -> llvm::raw_fd_ostream {
|
||||||
return llvm::raw_fd_ostream(fd_, /*shouldClose=*/false);
|
return llvm::raw_fd_ostream(fd_, /*shouldClose=*/false);
|
||||||
}
|
}
|
||||||
@@ -1495,6 +1550,14 @@ auto FileRef<A>::WriteFromBuffer(llvm::ArrayRef<std::byte> buffer)
|
|||||||
return FileRefBase::WriteFromBuffer(buffer);
|
return FileRefBase::WriteFromBuffer(buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
template <OpenAccess A>
|
||||||
|
auto FileRef<A>::WriteCompleteBuffer(llvm::ArrayRef<std::byte> buffer)
|
||||||
|
-> ErrorOr<Success, FdError>
|
||||||
|
requires Writeable
|
||||||
|
{
|
||||||
|
return FileRefBase::WriteCompleteBuffer(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
template <OpenAccess A>
|
template <OpenAccess A>
|
||||||
auto FileRef<A>::WriteStream() -> llvm::raw_fd_ostream
|
auto FileRef<A>::WriteStream() -> llvm::raw_fd_ostream
|
||||||
requires Writeable
|
requires Writeable
|
||||||
|
|||||||
@@ -411,6 +411,58 @@ TEST_F(FilesystemTest, WriteStream) {
|
|||||||
EXPECT_THAT(dir_.ReadFileToString("test"), IsSuccess(Eq(content_str)));
|
EXPECT_THAT(dir_.ReadFileToString("test"), IsSuccess(Eq(content_str)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_F(FilesystemTest, WriteCompleteBuffer) {
|
||||||
|
std::string content_str = "0123456789";
|
||||||
|
auto bytes = llvm::ArrayRef<std::byte>(
|
||||||
|
reinterpret_cast<const std::byte*>(content_str.data()),
|
||||||
|
content_str.size());
|
||||||
|
|
||||||
|
auto write = dir_.OpenWriteOnly("test", CreationOptions::CreateNew);
|
||||||
|
ASSERT_THAT(write, IsSuccess(_));
|
||||||
|
EXPECT_THAT(write->WriteCompleteBuffer(bytes), IsSuccess(_));
|
||||||
|
// Writing appends rather than replacing, unlike `WriteFileFromString`.
|
||||||
|
EXPECT_THAT(write->WriteCompleteBuffer(bytes), IsSuccess(_));
|
||||||
|
// An empty buffer is a no-op rather than an error.
|
||||||
|
EXPECT_THAT(write->WriteCompleteBuffer(llvm::ArrayRef<std::byte>()),
|
||||||
|
IsSuccess(_));
|
||||||
|
(*std::move(write)).Close().Check();
|
||||||
|
|
||||||
|
EXPECT_THAT(dir_.ReadFileToString("test"),
|
||||||
|
IsSuccess(Eq(content_str + content_str)));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(FilesystemTest, StandardStreams) {
|
||||||
|
// The standard streams name descriptors the process already has, so these
|
||||||
|
// are constants and never open or close anything.
|
||||||
|
static_assert(Stdin().unix_fd() == STDIN_FILENO);
|
||||||
|
static_assert(Stdout().unix_fd() == STDOUT_FILENO);
|
||||||
|
static_assert(Stderr().unix_fd() == STDERR_FILENO);
|
||||||
|
EXPECT_TRUE(Stderr().is_valid());
|
||||||
|
|
||||||
|
// Writing through one reaches the descriptor. Tests run with stdout captured,
|
||||||
|
// so this uses a pipe put in its place for the duration.
|
||||||
|
int fds[2];
|
||||||
|
ASSERT_EQ(pipe(fds), 0);
|
||||||
|
int saved = dup(STDOUT_FILENO);
|
||||||
|
ASSERT_GE(saved, 0);
|
||||||
|
ASSERT_GE(dup2(fds[1], STDOUT_FILENO), 0);
|
||||||
|
|
||||||
|
llvm::StringRef message = "through stdout";
|
||||||
|
auto result = Stdout().WriteCompleteBuffer(llvm::ArrayRef<std::byte>(
|
||||||
|
reinterpret_cast<const std::byte*>(message.data()), message.size()));
|
||||||
|
|
||||||
|
ASSERT_GE(dup2(saved, STDOUT_FILENO), 0);
|
||||||
|
ASSERT_EQ(close(saved), 0);
|
||||||
|
ASSERT_EQ(close(fds[1]), 0);
|
||||||
|
EXPECT_THAT(result, IsSuccess(_));
|
||||||
|
|
||||||
|
char buffer[64];
|
||||||
|
ssize_t n = read(fds[0], buffer, sizeof(buffer));
|
||||||
|
ASSERT_EQ(close(fds[0]), 0);
|
||||||
|
ASSERT_GT(n, 0);
|
||||||
|
EXPECT_EQ(llvm::StringRef(buffer, n), message);
|
||||||
|
}
|
||||||
|
|
||||||
TEST_F(FilesystemTest, Rename) {
|
TEST_F(FilesystemTest, Rename) {
|
||||||
// Rename a file within a directory.
|
// Rename a file within a directory.
|
||||||
ASSERT_THAT(dir_.WriteFileFromString("file1", "content1"), IsSuccess(_));
|
ASSERT_THAT(dir_.WriteFileFromString("file1", "content1"), IsSuccess(_));
|
||||||
|
|||||||
@@ -0,0 +1,195 @@
|
|||||||
|
# Part of the Carbon Language project, under the Apache License v2.0 with LLVM
|
||||||
|
# Exceptions. See /LICENSE for license information.
|
||||||
|
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
||||||
|
|
||||||
|
# Terminal rendering: what the attached terminal can do, and how to draw styled
|
||||||
|
# text for it. Used for diagnostic rendering and for command line output.
|
||||||
|
|
||||||
|
load("@rules_shell//shell:sh_test.bzl", "sh_test")
|
||||||
|
load("//bazel/cc_rules:defs.bzl", "cc_binary", "cc_library", "cc_test")
|
||||||
|
|
||||||
|
package(default_visibility = ["//visibility:public"])
|
||||||
|
|
||||||
|
cc_library(
|
||||||
|
name = "output_buffer_ref",
|
||||||
|
hdrs = ["output_buffer_ref.h"],
|
||||||
|
deps = ["@llvm-project//llvm:Support"],
|
||||||
|
)
|
||||||
|
|
||||||
|
cc_test(
|
||||||
|
name = "output_buffer_ref_test",
|
||||||
|
size = "small",
|
||||||
|
srcs = ["output_buffer_ref_test.cpp"],
|
||||||
|
deps = [
|
||||||
|
":output_buffer_ref",
|
||||||
|
"//testing/base:gtest_main",
|
||||||
|
"@googletest//:gtest",
|
||||||
|
"@llvm-project//llvm:Support",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
cc_library(
|
||||||
|
name = "color",
|
||||||
|
srcs = ["color.cpp"],
|
||||||
|
hdrs = ["color.h"],
|
||||||
|
deps = [
|
||||||
|
":output_buffer_ref",
|
||||||
|
"//common:check",
|
||||||
|
"//common:ostream",
|
||||||
|
"@llvm-project//llvm:Support",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
cc_test(
|
||||||
|
name = "color_test",
|
||||||
|
size = "small",
|
||||||
|
srcs = ["color_test.cpp"],
|
||||||
|
deps = [
|
||||||
|
":color",
|
||||||
|
"//common:ostream",
|
||||||
|
"//testing/base:gtest_main",
|
||||||
|
"@googletest//:gtest",
|
||||||
|
"@llvm-project//llvm:Support",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
cc_library(
|
||||||
|
name = "style",
|
||||||
|
srcs = ["style.cpp"],
|
||||||
|
hdrs = ["style.h"],
|
||||||
|
deps = [
|
||||||
|
":color",
|
||||||
|
":output_buffer_ref",
|
||||||
|
"//common:check",
|
||||||
|
"//common:ostream",
|
||||||
|
"@llvm-project//llvm:Support",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
cc_test(
|
||||||
|
name = "style_test",
|
||||||
|
size = "small",
|
||||||
|
srcs = ["style_test.cpp"],
|
||||||
|
deps = [
|
||||||
|
":style",
|
||||||
|
"//common:ostream",
|
||||||
|
"//common:raw_string_ostream",
|
||||||
|
"//testing/base:gtest_main",
|
||||||
|
"@googletest//:gtest",
|
||||||
|
"@llvm-project//llvm:Support",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
cc_library(
|
||||||
|
name = "capabilities",
|
||||||
|
srcs = ["capabilities.cpp"],
|
||||||
|
hdrs = ["capabilities.h"],
|
||||||
|
deps = [
|
||||||
|
":color",
|
||||||
|
"//common:filesystem",
|
||||||
|
"@llvm-project//llvm:Support",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
cc_test(
|
||||||
|
name = "capabilities_test",
|
||||||
|
size = "small",
|
||||||
|
srcs = ["capabilities_test.cpp"],
|
||||||
|
deps = [
|
||||||
|
":capabilities",
|
||||||
|
"//common:filesystem",
|
||||||
|
"//testing/base:gtest_main",
|
||||||
|
"@googletest//:gtest",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
cc_library(
|
||||||
|
name = "metrics",
|
||||||
|
srcs = ["metrics.cpp"],
|
||||||
|
hdrs = ["metrics.h"],
|
||||||
|
deps = [
|
||||||
|
":capabilities",
|
||||||
|
"//common:check",
|
||||||
|
"@llvm-project//llvm:Support",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
cc_test(
|
||||||
|
name = "metrics_test",
|
||||||
|
size = "small",
|
||||||
|
srcs = ["metrics_test.cpp"],
|
||||||
|
deps = [
|
||||||
|
":metrics",
|
||||||
|
"//testing/base:gtest_main",
|
||||||
|
"@googletest//:gtest",
|
||||||
|
"@llvm-project//llvm:Support",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
cc_library(
|
||||||
|
name = "buffer",
|
||||||
|
srcs = ["buffer.cpp"],
|
||||||
|
hdrs = ["buffer.h"],
|
||||||
|
deps = [
|
||||||
|
":capabilities",
|
||||||
|
":color",
|
||||||
|
":metrics",
|
||||||
|
":output_buffer_ref",
|
||||||
|
":style",
|
||||||
|
"//common:check",
|
||||||
|
"//common:filesystem",
|
||||||
|
"@llvm-project//llvm:Support",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
cc_test(
|
||||||
|
name = "buffer_test",
|
||||||
|
size = "small",
|
||||||
|
srcs = ["buffer_test.cpp"],
|
||||||
|
deps = [
|
||||||
|
":buffer",
|
||||||
|
":metrics",
|
||||||
|
"//common:filesystem",
|
||||||
|
"//testing/base:gtest_main",
|
||||||
|
"@googletest//:gtest",
|
||||||
|
"@llvm-project//llvm:Support",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
cc_test(
|
||||||
|
name = "pressure_test",
|
||||||
|
size = "small",
|
||||||
|
srcs = ["pressure_test.cpp"],
|
||||||
|
deps = [
|
||||||
|
":buffer",
|
||||||
|
":capabilities",
|
||||||
|
":metrics",
|
||||||
|
":style",
|
||||||
|
"//testing/base:gtest_main",
|
||||||
|
"@googletest//:gtest",
|
||||||
|
"@llvm-project//llvm:Support",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
cc_binary(
|
||||||
|
name = "terminal_benchmark",
|
||||||
|
testonly = 1,
|
||||||
|
srcs = ["terminal_benchmark.cpp"],
|
||||||
|
deps = [
|
||||||
|
":buffer",
|
||||||
|
":capabilities",
|
||||||
|
":color",
|
||||||
|
":style",
|
||||||
|
"//testing/base:benchmark_main",
|
||||||
|
"@abseil-cpp//absl/random",
|
||||||
|
"@google_benchmark//:benchmark",
|
||||||
|
"@llvm-project//llvm:Support",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
sh_test(
|
||||||
|
name = "terminal_benchmark_test",
|
||||||
|
size = "small",
|
||||||
|
srcs = [":terminal_benchmark"],
|
||||||
|
args = ["--benchmark_dry_run"],
|
||||||
|
)
|
||||||
@@ -0,0 +1,554 @@
|
|||||||
|
// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
|
||||||
|
// Exceptions. See /LICENSE for license information.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
||||||
|
|
||||||
|
#include "common/terminal/buffer.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include "common/check.h"
|
||||||
|
#include "llvm/ADT/STLExtras.h"
|
||||||
|
#include "llvm/ADT/Sequence.h"
|
||||||
|
#include "llvm/ADT/SmallString.h"
|
||||||
|
#include "llvm/Support/ConvertUTF.h"
|
||||||
|
#include "llvm/Support/Unicode.h"
|
||||||
|
|
||||||
|
namespace Carbon::Terminal {
|
||||||
|
|
||||||
|
// The most bytes of combining marks kept on one cell. Text stacking more than
|
||||||
|
// this is either adversarial or already illegible, and keeping all of it would
|
||||||
|
// let a single column of output carry unbounded bytes.
|
||||||
|
static constexpr size_t MaxCombiningBytes = 32;
|
||||||
|
|
||||||
|
// Glyphs for every combination of line directions, indexed by the direction
|
||||||
|
// bits.
|
||||||
|
static constexpr std::array<char32_t, 16> Utf8LineGlyphs = {
|
||||||
|
U'·', // (none): a line between one center and itself, which is 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
|
||||||
|
};
|
||||||
|
|
||||||
|
// The ASCII stand-ins, which can only distinguish horizontal, vertical, and
|
||||||
|
// everything else.
|
||||||
|
static constexpr std::array<char32_t, 16> AsciiLineGlyphs = {
|
||||||
|
U'+', U'-', U'-', U'-', U'|', U'+', U'+', U'+',
|
||||||
|
U'|', U'+', U'+', U'+', U'|', U'+', U'+', U'+',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Returns the next tab stop after `x` on a line whose stops are `tab_width`
|
||||||
|
// columns apart counting from `origin`, which `x` must not be left of.
|
||||||
|
static auto NextTabStop(int x, int origin, int tab_width) -> int {
|
||||||
|
CARBON_DCHECK(x >= origin, "Column {0} is left of the origin {1}.", x,
|
||||||
|
origin);
|
||||||
|
return origin + ((x - origin) / tab_width + 1) * tab_width;
|
||||||
|
}
|
||||||
|
|
||||||
|
Buffer::Buffer(int columns, Charset charset, int tab_width)
|
||||||
|
: columns_(columns),
|
||||||
|
width_(columns),
|
||||||
|
tab_width_(tab_width),
|
||||||
|
metrics_(charset) {
|
||||||
|
CARBON_CHECK(columns > 0 && columns <= MaxColumns,
|
||||||
|
"Buffer width must be in [1, {0}], but was {1}.", MaxColumns,
|
||||||
|
columns);
|
||||||
|
CARBON_CHECK(tab_width > 0 && tab_width <= MaxTabWidth,
|
||||||
|
"Tab width must be in [1, {0}], but was {1}.", MaxTabWidth,
|
||||||
|
tab_width);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Buffer::height() const -> int {
|
||||||
|
return static_cast<int>(cells_.size()) / width_;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Buffer::EnsureRow(int y) -> void {
|
||||||
|
CARBON_CHECK(y >= 0 && y < MaxRows, "Row {0} is outside [0, {1}).", y,
|
||||||
|
MaxRows);
|
||||||
|
if (y < height()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Rows are added at the end and nothing already in the grid moves, so this
|
||||||
|
// asks for exactly the rows wanted and lets the vector amortize the growing.
|
||||||
|
cells_.resize(static_cast<size_t>(y + 1) * width_);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Buffer::EnsureColumn(int x) -> void {
|
||||||
|
CARBON_CHECK(x >= 0 && x < MaxColumns, "Column {0} is outside [0, {1}).", x,
|
||||||
|
MaxColumns);
|
||||||
|
if (x < width_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Widening moves every row, so it grows by halves rather than to exactly what
|
||||||
|
// was asked: a row drawn one code point at a time would otherwise copy the
|
||||||
|
// whole grid on every one of them. Growth stops at the bound, which is what
|
||||||
|
// holds the product of the two dimensions inside what a cell index can
|
||||||
|
// represent.
|
||||||
|
int width = std::min(std::max(x + 1, width_ + width_ / 2), MaxColumns);
|
||||||
|
|
||||||
|
int rows = height();
|
||||||
|
llvm::SmallVector<Cell, 0> new_cells(static_cast<size_t>(rows) * width);
|
||||||
|
for (int y : llvm::seq(rows)) {
|
||||||
|
llvm::copy(
|
||||||
|
llvm::ArrayRef(cells_).slice(static_cast<size_t>(y) * width_, width_),
|
||||||
|
new_cells.begin() + static_cast<size_t>(y) * width);
|
||||||
|
}
|
||||||
|
cells_ = std::move(new_cells);
|
||||||
|
|
||||||
|
// A mark's key is a cell index, which depends on the width, so each is
|
||||||
|
// recomputed for the new one.
|
||||||
|
llvm::DenseMap<int, std::string> new_combining_marks;
|
||||||
|
new_combining_marks.reserve(combining_marks_.size());
|
||||||
|
for (auto& [index, marks] : combining_marks_) {
|
||||||
|
new_combining_marks.insert(
|
||||||
|
{index / width_ * width + index % width_, std::move(marks)});
|
||||||
|
}
|
||||||
|
combining_marks_ = std::move(new_combining_marks);
|
||||||
|
|
||||||
|
width_ = width;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Buffer::ClearCells(int x, int y, int width) -> void {
|
||||||
|
CARBON_CHECK(
|
||||||
|
x >= 0 && width >= 0 && x + width <= width_ && y >= 0 && y < height(),
|
||||||
|
"Clearing [{0}, {1}) of row {2} reaches outside the {3}x{4} cells the "
|
||||||
|
"buffer holds.",
|
||||||
|
x, x + width, y, width_, height());
|
||||||
|
|
||||||
|
// A cleared range must not leave half of a double-width character behind, so
|
||||||
|
// it extends over either half that crosses its edges.
|
||||||
|
int begin = x;
|
||||||
|
if (begin > 0 && CellAt(begin, y).is_continuation) {
|
||||||
|
--begin;
|
||||||
|
}
|
||||||
|
int end = x + width;
|
||||||
|
if (end < width_ && CellAt(end, y).is_continuation) {
|
||||||
|
++end;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = begin; i < end; ++i) {
|
||||||
|
CellAt(i, y) = Cell();
|
||||||
|
combining_marks_.erase(CellIndex(i, y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Buffer::AttachCombiningMark(int x, int y, char32_t code_point) -> void {
|
||||||
|
// A mark has nowhere to go when no cell precedes it, so it is dropped.
|
||||||
|
if (x <= 0 || x > width_ || y < 0 || y >= height()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// The left half of a double-width character is never itself a continuation,
|
||||||
|
// so stepping back from one always lands on a real character.
|
||||||
|
int base = x - 1;
|
||||||
|
if (CellAt(base, y).is_continuation) {
|
||||||
|
--base;
|
||||||
|
}
|
||||||
|
CARBON_CHECK(base >= 0, "A continuation cell at column zero has no base.");
|
||||||
|
|
||||||
|
Utf8Storage storage;
|
||||||
|
llvm::StringRef encoded = EncodeUtf8(code_point, storage);
|
||||||
|
std::string& marks = combining_marks_[CellIndex(base, y)];
|
||||||
|
if (marks.size() + encoded.size() > MaxCombiningBytes) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
marks.append(encoded.data(), encoded.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Buffer::DrawCodePoint(int x, int y, char32_t code_point,
|
||||||
|
const Style& style) -> DrawEnd {
|
||||||
|
CheckOrigin(x, y);
|
||||||
|
return {.x = PlaceCodePoint(x, y, code_point, style), .y = y};
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Buffer::PlaceCodePoint(int x, int y, char32_t code_point,
|
||||||
|
const Style& style) -> int {
|
||||||
|
CARBON_DCHECK(x >= 0 && y >= 0,
|
||||||
|
"Placing at ({0}, {1}), which no walk should reach.", x, y);
|
||||||
|
|
||||||
|
int width = metrics_.CodePointWidth(code_point);
|
||||||
|
if (width == 0) {
|
||||||
|
AttachCombiningMark(x, y, code_point);
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
code_point = metrics_.RenderedCodePoint(code_point);
|
||||||
|
|
||||||
|
// Both bounds are reached by what the text holds rather than by where the
|
||||||
|
// caller aimed -- a word overhanging the target width, or newlines running
|
||||||
|
// past the rows a grid can index -- so past either one nothing is drawn and
|
||||||
|
// the column still advances, which is what keeps measuring and drawing
|
||||||
|
// answering the same thing. A double-width character needs both its columns,
|
||||||
|
// so one that would only half fit is past the edge like any other: splitting
|
||||||
|
// it would leave the terminal rendering half a character.
|
||||||
|
if (y >= MaxRows || x > MaxColumns - width) {
|
||||||
|
return x + width;
|
||||||
|
}
|
||||||
|
|
||||||
|
EnsureColumn(x + width - 1);
|
||||||
|
EnsureRow(y);
|
||||||
|
ClearCells(x, y, width);
|
||||||
|
|
||||||
|
Cell& cell = CellAt(x, y);
|
||||||
|
cell.code_point = code_point;
|
||||||
|
cell.style = style;
|
||||||
|
// Nothing is wider than two columns, so the second is the only continuation
|
||||||
|
// there can be.
|
||||||
|
if (width > 1) {
|
||||||
|
Cell& continuation = CellAt(x + 1, y);
|
||||||
|
continuation.style = style;
|
||||||
|
continuation.is_continuation = true;
|
||||||
|
}
|
||||||
|
return x + width;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the glyphs a cell's directions are read from.
|
||||||
|
static auto LineGlyphs(Charset charset) -> const std::array<char32_t, 16>& {
|
||||||
|
return charset == Charset::Utf8 ? Utf8LineGlyphs : AsciiLineGlyphs;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Buffer::DrawLine(int x, int y, uint8_t directions, const Style& style)
|
||||||
|
-> void {
|
||||||
|
CARBON_DCHECK(directions <= LineDirections,
|
||||||
|
"Direction bits {0} name no glyph.", directions);
|
||||||
|
EnsureColumn(x);
|
||||||
|
EnsureRow(y);
|
||||||
|
|
||||||
|
uint8_t existing = CellAt(x, y).lines;
|
||||||
|
if (existing == 0) {
|
||||||
|
// Whatever is here isn't a line. Clearing also removes either half of a
|
||||||
|
// double-width character the cell was part of.
|
||||||
|
ClearCells(x, y, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
Cell& cell = CellAt(x, y);
|
||||||
|
cell.lines = existing | directions | LineCell;
|
||||||
|
cell.code_point = LineGlyphs(metrics_.charset())[cell.lines & LineDirections];
|
||||||
|
cell.style = style;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks that a line of `length` starting at `position` stays within `limit`,
|
||||||
|
// which is the width for a horizontal line and `MaxRows` for a vertical one.
|
||||||
|
//
|
||||||
|
// Unlike text, a line has no reason to reach outside what it is being drawn
|
||||||
|
// into: nothing about it is unbreakable, and a layout that put one there
|
||||||
|
// computed the wrong extent.
|
||||||
|
static auto CheckLineFits(int position, int length, int limit) -> void {
|
||||||
|
CARBON_CHECK(length >= 0 && position <= limit - length,
|
||||||
|
"A line of {0} at {1} runs outside the {2} available to it.",
|
||||||
|
length, position, limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Buffer::DrawHorizontalLine(int x, int y, int length, const Style& style,
|
||||||
|
LineEnd start, LineEnd end) -> DrawEnd {
|
||||||
|
CheckOrigin(x, y);
|
||||||
|
CheckLineFits(x, length, columns_);
|
||||||
|
for (int i : llvm::seq(length)) {
|
||||||
|
// A cell in the middle of the line is entered from one side and left by the
|
||||||
|
// other. An end cell is only left towards the rest of the line, unless that
|
||||||
|
// end runs out through the cell's own side.
|
||||||
|
uint8_t directions =
|
||||||
|
(i > 0 || start == LineEnd::Edge ? LineLeft : 0) |
|
||||||
|
(i + 1 < length || end == LineEnd::Edge ? LineRight : 0);
|
||||||
|
DrawLine(x + i, y, directions, style);
|
||||||
|
}
|
||||||
|
return {.x = x + length, .y = y};
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Buffer::DrawVerticalLine(int x, int y, int length, const Style& style,
|
||||||
|
LineEnd start, LineEnd end) -> DrawEnd {
|
||||||
|
CheckOrigin(x, y);
|
||||||
|
CheckLineFits(y, length, MaxRows);
|
||||||
|
for (int i : llvm::seq(length)) {
|
||||||
|
uint8_t directions =
|
||||||
|
(i > 0 || start == LineEnd::Edge ? LineUp : 0) |
|
||||||
|
(i + 1 < length || end == LineEnd::Edge ? LineDown : 0);
|
||||||
|
DrawLine(x, y + i, directions, style);
|
||||||
|
}
|
||||||
|
return {.x = x, .y = y + length};
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Buffer::DrawBox(int x, int y, int box_width, int box_height,
|
||||||
|
const Style& style) -> DrawEnd {
|
||||||
|
CheckOrigin(x, y);
|
||||||
|
CheckLineFits(x, box_width, columns_);
|
||||||
|
CheckLineFits(y, box_height, MaxRows);
|
||||||
|
if (box_width == 0 || box_height == 0) {
|
||||||
|
return {.x = x, .y = y};
|
||||||
|
}
|
||||||
|
DrawHorizontalLine(x, y, box_width, style);
|
||||||
|
DrawHorizontalLine(x, y + box_height - 1, box_width, style);
|
||||||
|
DrawVerticalLine(x, y, box_height, style);
|
||||||
|
DrawVerticalLine(x + box_width - 1, y, box_height, style);
|
||||||
|
return {.x = x + box_width, .y = y + box_height};
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
int cur_x = x;
|
||||||
|
int cur_y = y;
|
||||||
|
|
||||||
|
while (!text.empty()) {
|
||||||
|
char32_t code_point = metrics_.TakeCodePoint(text);
|
||||||
|
if (code_point == '\n') {
|
||||||
|
cur_x = margin;
|
||||||
|
++cur_y;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (code_point == '\r') {
|
||||||
|
cur_x = margin;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (code_point == '\t') {
|
||||||
|
int stop = NextTabStop(cur_x, margin, tab_width_);
|
||||||
|
for (; cur_x < stop; ++cur_x) {
|
||||||
|
place(cur_x, cur_y, U' ');
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
cur_x = place(cur_x, cur_y, code_point);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {.x = cur_x, .y = cur_y};
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Buffer::DrawText(int x, int y, int margin, llvm::StringRef text,
|
||||||
|
const Style& style) -> DrawEnd {
|
||||||
|
return WalkText(x, y, margin, text,
|
||||||
|
[&](int cur_x, int cur_y, char32_t code_point) {
|
||||||
|
return PlaceCodePoint(cur_x, cur_y, code_point, style);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Buffer::MeasureText(int x, int y, int margin, llvm::StringRef text) const
|
||||||
|
-> DrawEnd {
|
||||||
|
return WalkText(x, y, margin, text,
|
||||||
|
[&](int cur_x, int /*cur_y*/, char32_t code_point) {
|
||||||
|
return cur_x + metrics_.CodePointWidth(code_point);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns whether wrapped text can be broken at `c`.
|
||||||
|
//
|
||||||
|
// This is the one definition of where wrapping may introduce a break, so that
|
||||||
|
// measuring what text wraps into and drawing it wrapped agree about it.
|
||||||
|
// Carriage returns count so that a CRLF ending is whitespace rather than part
|
||||||
|
// of the word before it; what becomes of the `\r` is then up to the drawing.
|
||||||
|
static constexpr auto IsWrapBreak(char c) -> bool {
|
||||||
|
return c == ' ' || c == '\t' || c == '\r';
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename PlaceFn>
|
||||||
|
auto Buffer::WalkWrappedText(int x, int y, int margin, int max_width,
|
||||||
|
llvm::StringRef text, PlaceFn place) const
|
||||||
|
-> DrawEnd {
|
||||||
|
CheckTextSize(text);
|
||||||
|
// The block runs from the margin to `margin + max_width`, lies within the
|
||||||
|
// buffer, and holds the column the text starts in, which is every bound on
|
||||||
|
// the three of them read in one order.
|
||||||
|
CARBON_CHECK(llvm::is_sorted(std::array{0, margin, x, x + 1,
|
||||||
|
margin + max_width, columns_}) &&
|
||||||
|
y >= 0 && y < MaxRows,
|
||||||
|
"A block of {0} columns at {1} holding text from ({2}, {3}) "
|
||||||
|
"does not fit the {4} columns and {5} rows a buffer covers.",
|
||||||
|
max_width, margin, x, y, columns_, MaxRows);
|
||||||
|
|
||||||
|
// The column a row runs out of room at. The block lies within the buffer's
|
||||||
|
// width, so this is a column like any other rather than a sum that has to be
|
||||||
|
// kept from overflowing.
|
||||||
|
int limit = margin + max_width;
|
||||||
|
|
||||||
|
int cur_x = x;
|
||||||
|
int cur_y = y;
|
||||||
|
|
||||||
|
// Splitting on bytes is safe because every character text can break at is
|
||||||
|
// ASCII, and UTF-8 never encodes anything else using an ASCII byte. Only
|
||||||
|
// words are decoded; whitespace is handled a byte at a time.
|
||||||
|
while (!text.empty()) {
|
||||||
|
if (text.front() == '\n') {
|
||||||
|
text = text.drop_front();
|
||||||
|
cur_x = margin;
|
||||||
|
++cur_y;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsWrapBreak(text.front())) {
|
||||||
|
llvm::StringRef breaks = text.take_while(IsWrapBreak);
|
||||||
|
text = text.drop_front(breaks.size());
|
||||||
|
for (char c : breaks) {
|
||||||
|
if (c == '\r') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Whitespace stops at the block's edge, leaving the word after it to
|
||||||
|
// wrap.
|
||||||
|
int next = std::min(
|
||||||
|
c == '\t' ? NextTabStop(cur_x, margin, tab_width_) : cur_x + 1,
|
||||||
|
limit);
|
||||||
|
while (cur_x < next) {
|
||||||
|
cur_x = place(cur_x, cur_y, U' ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A combining mark renders into the column before it, so one following
|
||||||
|
// whitespace belongs to that whitespace and goes with it. Left to begin
|
||||||
|
// the next word, it would move to another row whenever that word wrapped
|
||||||
|
// and attach to whatever preceded it there.
|
||||||
|
while (!text.empty()) {
|
||||||
|
llvm::StringRef rest = text;
|
||||||
|
char32_t code_point = metrics_.TakeCodePoint(rest);
|
||||||
|
if (metrics_.CodePointWidth(code_point) != 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
text = rest;
|
||||||
|
cur_x = place(cur_x, cur_y, code_point);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
llvm::StringRef word =
|
||||||
|
text.take_until([](char c) { return c == '\n' || IsWrapBreak(c); });
|
||||||
|
text = text.drop_front(word.size());
|
||||||
|
|
||||||
|
// Move a word that doesn't fit down to the next row, which minimizes the
|
||||||
|
// overhang when it doesn't fit there either. The word is drawn into the row
|
||||||
|
// this starts before anything else can reach it, so a wrapped row begins at
|
||||||
|
// the margin rather than with the whitespace the wrap came after.
|
||||||
|
if (cur_x > margin && cur_x + metrics_.Width(word) > limit) {
|
||||||
|
cur_x = margin;
|
||||||
|
++cur_y;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (!word.empty()) {
|
||||||
|
cur_x = place(cur_x, cur_y, metrics_.TakeCodePoint(word));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {.x = cur_x, .y = cur_y};
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Buffer::DrawWrappedText(int x, int y, int margin, int max_width,
|
||||||
|
llvm::StringRef text, const Style& style)
|
||||||
|
-> DrawEnd {
|
||||||
|
return WalkWrappedText(x, y, margin, max_width, text,
|
||||||
|
[&](int cur_x, int cur_y, char32_t code_point) {
|
||||||
|
return PlaceCodePoint(cur_x, cur_y, code_point,
|
||||||
|
style);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Buffer::MeasureWrappedText(int x, int y, int margin, int max_width,
|
||||||
|
llvm::StringRef text) const -> DrawEnd {
|
||||||
|
return WalkWrappedText(x, y, margin, max_width, text,
|
||||||
|
[&](int cur_x, int /*cur_y*/, char32_t code_point) {
|
||||||
|
return cur_x + metrics_.CodePointWidth(code_point);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Buffer::MeasureWrapWidth(llvm::StringRef text) const -> int {
|
||||||
|
int width = 0;
|
||||||
|
while (!text.empty()) {
|
||||||
|
llvm::StringRef word =
|
||||||
|
text.take_until([](char c) { return c == '\n' || IsWrapBreak(c); });
|
||||||
|
width = std::max(width, metrics_.Width(word));
|
||||||
|
text = text.drop_front(std::max<size_t>(word.size(), 1));
|
||||||
|
}
|
||||||
|
return width;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Buffer::LastVisibleColumn(int y, ColorMode mode) const -> int {
|
||||||
|
// A style only paints a blank cell if it is rendered at all, so with color
|
||||||
|
// off a blank cell is padding whatever style it carries.
|
||||||
|
bool styles_render = mode != ColorMode::NoColor;
|
||||||
|
for (int x = width_ - 1; x >= 0; --x) {
|
||||||
|
const Cell& cell = CellAt(x, y);
|
||||||
|
if (cell.is_continuation || cell.code_point != ' ' ||
|
||||||
|
(styles_render && cell.style.IsVisibleOnBlank()) ||
|
||||||
|
(!combining_marks_.empty() &&
|
||||||
|
combining_marks_.contains(CellIndex(x, y)))) {
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Buffer::Render(OutputBufferRef out, ColorMode mode) const -> void {
|
||||||
|
Utf8Storage storage;
|
||||||
|
|
||||||
|
// The style a terminal starts in, and the one it is left in.
|
||||||
|
const Style default_style;
|
||||||
|
|
||||||
|
// Cells outlive this loop, so the active style is tracked by pointing at one
|
||||||
|
// rather than copying a whole style per cell. It carries across rows: a style
|
||||||
|
// is usually still in use on the row below, and turning it off and back on
|
||||||
|
// costs a reset and a fresh start for nothing.
|
||||||
|
const Style* active = &default_style;
|
||||||
|
|
||||||
|
int rows = height();
|
||||||
|
for (int y = 0; y < rows; ++y) {
|
||||||
|
int last = LastVisibleColumn(y, mode);
|
||||||
|
for (int x = 0; x <= last; ++x) {
|
||||||
|
const Cell& cell = CellAt(x, y);
|
||||||
|
if (cell.is_continuation) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
active->AppendTransitionTo(out, cell.style, mode);
|
||||||
|
active = &cell.style;
|
||||||
|
out.Append(EncodeUtf8(cell.code_point, storage));
|
||||||
|
|
||||||
|
// Almost nothing has combining marks, so the lookup is worth skipping
|
||||||
|
// outright rather than doing it for every cell on the screen.
|
||||||
|
if (!combining_marks_.empty()) {
|
||||||
|
auto marks = combining_marks_.find(CellIndex(x, y));
|
||||||
|
if (marks != combining_marks_.end()) {
|
||||||
|
out.Append(marks->second);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A style is turned off before the newline in two cases. On the last row,
|
||||||
|
// so that nothing is left set for whatever is printed after this and the
|
||||||
|
// escape that turns it off still falls inside the rendering. And whenever
|
||||||
|
// it paints where there is no glyph, because a terminal fills the rest of
|
||||||
|
// the row with the background it is in when the row ends, so leaving one
|
||||||
|
// set would paint a stripe out to the right edge that nothing asked for.
|
||||||
|
if (y + 1 == rows || active->IsVisibleOnBlank()) {
|
||||||
|
active->AppendTransitionTo(out, default_style, mode);
|
||||||
|
active = &default_style;
|
||||||
|
}
|
||||||
|
out.Append("\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Buffer::WriteTo(Filesystem::WriteFileRef file, ColorMode mode) const
|
||||||
|
-> ErrorOr<Success, Filesystem::FdError> {
|
||||||
|
// Sized for the few short lines a diagnostic renders to. A full screen with
|
||||||
|
// color runs well past it and allocates once.
|
||||||
|
llvm::SmallString<1024> bytes;
|
||||||
|
Render(bytes, mode);
|
||||||
|
return file.WriteCompleteBuffer(llvm::ArrayRef<std::byte>(
|
||||||
|
reinterpret_cast<const std::byte*>(bytes.data()), bytes.size()));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Carbon::Terminal
|
||||||
@@ -0,0 +1,537 @@
|
|||||||
|
// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
|
||||||
|
// Exceptions. See /LICENSE for license information.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
||||||
|
|
||||||
|
#ifndef CARBON_COMMON_TERMINAL_BUFFER_H_
|
||||||
|
#define CARBON_COMMON_TERMINAL_BUFFER_H_
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "common/check.h"
|
||||||
|
#include "common/filesystem.h"
|
||||||
|
#include "common/terminal/capabilities.h"
|
||||||
|
#include "common/terminal/color.h"
|
||||||
|
#include "common/terminal/metrics.h"
|
||||||
|
#include "common/terminal/output_buffer_ref.h"
|
||||||
|
#include "common/terminal/style.h"
|
||||||
|
#include "llvm/ADT/DenseMap.h"
|
||||||
|
#include "llvm/ADT/SmallVector.h"
|
||||||
|
#include "llvm/ADT/StringRef.h"
|
||||||
|
|
||||||
|
namespace Carbon::Terminal {
|
||||||
|
|
||||||
|
// Where a line stops within the cell at one of its ends.
|
||||||
|
//
|
||||||
|
// A line runs between points, and in a grid of cells the two points it can
|
||||||
|
// name are a cell's center and a cell's outer edge. Which one an end is decides
|
||||||
|
// what a line meeting it there becomes: a line ending at a center and another
|
||||||
|
// leaving that center form a corner, while a line running out through an edge
|
||||||
|
// carries on past whatever meets it, which is a tee.
|
||||||
|
//
|
||||||
|
// This is the distinction a vector graphics stroke draws between a butt cap and
|
||||||
|
// a square cap, where the square cap extends the stroke by half its width past
|
||||||
|
// the endpoint. Half a stroke here is half a cell.
|
||||||
|
//
|
||||||
|
// Unicode has a glyph for a line reaching only the middle of its cell (U+2574
|
||||||
|
// through U+2577), so a `Center` end is drawn as one and the reader sees where
|
||||||
|
// the line really stops rather than having to infer it from the junctions. With
|
||||||
|
// `Charset::Ascii` there is nothing to draw half a line with, so both ends fill
|
||||||
|
// their cell and only the junctions around them say which was which.
|
||||||
|
enum class LineEnd : int8_t {
|
||||||
|
// The line stops at the center of its end cell. Lines meeting there corner.
|
||||||
|
Center,
|
||||||
|
// The line runs out through the outer edge of its end cell, joining whatever
|
||||||
|
// is beyond it. Lines meeting there tee.
|
||||||
|
Edge,
|
||||||
|
};
|
||||||
|
|
||||||
|
// A grid of styled cells staged for rendering to a terminal.
|
||||||
|
//
|
||||||
|
// Coordinates are 0-based with (0, 0) at the top left, `x` counting terminal
|
||||||
|
// columns and `y` counting rows.
|
||||||
|
//
|
||||||
|
// A buffer renders once, top to bottom, the way a compiler writes diagnostics.
|
||||||
|
// There is no cursor addressing and nothing is ever redrawn, so a rendered
|
||||||
|
// buffer is just as valid in a file or a pipe as on a terminal.
|
||||||
|
//
|
||||||
|
// Every row is a line, ended by a newline of its own, so nothing is left for
|
||||||
|
// the terminal to break. A break introduced to fit a width is an ordinary
|
||||||
|
// newline like any other, which is what lets wrapped text carry an indent or
|
||||||
|
// sit in a column beside a gutter: a terminal wrapping a row of its own accord
|
||||||
|
// continues at column zero, under the gutter rather than beside it. It also
|
||||||
|
// means text copied out of the output holds the lines that were displayed.
|
||||||
|
//
|
||||||
|
// The cost is that such a break is in whatever a reader copies, so wrapping
|
||||||
|
// never puts one inside a word. A path or a URL stays whole and overhangs the
|
||||||
|
// width when it doesn't fit, which is what keeps it selectable in one piece and
|
||||||
|
// clickable where a terminal recognizes one. Wrapping only adds breaks as well:
|
||||||
|
// the newlines already in a caller's text are kept as they are. A row is a row
|
||||||
|
// once something is drawn into it, so a break the text ends with closes its
|
||||||
|
// last line rather than opening an empty one after it.
|
||||||
|
//
|
||||||
|
// Staging into a grid lets layout position content directly, rather than
|
||||||
|
// interleaving text, padding, and escape sequences as it goes. That separation
|
||||||
|
// is what makes the two hard parts tractable: escape sequences are minimized
|
||||||
|
// once, in `Render`, and the drawing APIs reason about columns on screen rather
|
||||||
|
// than bytes in a stream.
|
||||||
|
//
|
||||||
|
// Which bytes make up a column depends on the charset, and the buffer handles
|
||||||
|
// that rather than leaving it to callers, because getting it wrong misaligns
|
||||||
|
// everything downstream of it:
|
||||||
|
//
|
||||||
|
// - Under `Charset::Ascii` no UTF-8 processing happens at all. Every byte is
|
||||||
|
// one column, exactly as a terminal decoding some single-byte encoding will
|
||||||
|
// treat it, and bytes outside printable ASCII are replaced with `?` because
|
||||||
|
// there is no telling what such a terminal would draw for them.
|
||||||
|
// - Under `Charset::Utf8` bytes are decoded as UTF-8. Double-width characters
|
||||||
|
// occupy both of the columns they will really take, and drawing over either
|
||||||
|
// column erases the whole character instead of leaving half of one behind.
|
||||||
|
// Combining marks render into the column before them, so a base character
|
||||||
|
// and its marks stay in one cell. Carbon source is in Unicode normalization
|
||||||
|
// form C, which still spells out marks for characters that have no
|
||||||
|
// precomposed form, so this comes up in ordinary input. Anything with no
|
||||||
|
// printable rendering, including invalid UTF-8, becomes U+FFFD.
|
||||||
|
//
|
||||||
|
// A buffer is `columns()` wide, and that width is the whole point of it: it is
|
||||||
|
// what wrapping fits text into, and it comes from the terminal where one was
|
||||||
|
// measured and from `DefaultColumns` where none was. Rows are the direction
|
||||||
|
// there is no bound in -- a buffer grows downward to whatever is drawn into it,
|
||||||
|
// 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.
|
||||||
|
//
|
||||||
|
// 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 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
|
||||||
|
// go and is dropped. That is data rather than a coordinate, which is why it is
|
||||||
|
// dropped rather than checked: source files contain such text.
|
||||||
|
//
|
||||||
|
// TODO: None of this handles bidirectional text. A right-to-left run reorders
|
||||||
|
// on screen, so the column a character occupies stops following from the
|
||||||
|
// characters before it, which is the assumption every position here rests on:
|
||||||
|
// that drawing advances left to right by the width of what was drawn. Getting
|
||||||
|
// this right needs the reordering to happen before anything is placed, which
|
||||||
|
// makes it a question about where the boundary between a client's layout and
|
||||||
|
// this buffer should sit -- whether the buffer takes runs that are already in
|
||||||
|
// visual order, or takes logical order and reorders as it draws, and what it
|
||||||
|
// then means for a caller to name a column at all. Marking a span and drawing a
|
||||||
|
// line under it are the hard cases, since a logically contiguous span need not
|
||||||
|
// be contiguous on screen.
|
||||||
|
class Buffer {
|
||||||
|
public:
|
||||||
|
// 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.
|
||||||
|
static constexpr int MaxColumns = 1 << 14;
|
||||||
|
static constexpr int MaxRows = 1 << 16;
|
||||||
|
|
||||||
|
// The most bytes of text one operation draws or measures.
|
||||||
|
//
|
||||||
|
// The column advances by the width of what was drawn whether or not a cell
|
||||||
|
// was written, so without this a long enough run would carry it past what an
|
||||||
|
// `int` holds and come back negative. Far more text than any terminal shows,
|
||||||
|
// and a caller with this much has built it rather than read it off a line.
|
||||||
|
static constexpr int MaxTextBytes = 1 << 24;
|
||||||
|
|
||||||
|
// The widest tab stops a buffer draws to.
|
||||||
|
//
|
||||||
|
// Far past any terminal, and small enough that even text made entirely of
|
||||||
|
// tabs measures into a column an `int` holds: a tab is the one character
|
||||||
|
// that occupies more columns than it does bytes, so this is what bounds
|
||||||
|
// `MaxTextBytes` of them.
|
||||||
|
static constexpr int MaxTabWidth = 64;
|
||||||
|
|
||||||
|
// Where a drawing ended: for text, the row it ended on and the column after
|
||||||
|
// its last code point there; for a line or a box, the cell past the end of
|
||||||
|
// what it drew.
|
||||||
|
//
|
||||||
|
// Everything that draws returns one, so that a caller placing something
|
||||||
|
// after a drawing advances from this rather than measuring the same text a
|
||||||
|
// second time. The `Measure` operations return one too, and answer for text
|
||||||
|
// that hasn't been drawn yet what drawing it would answer.
|
||||||
|
struct DrawEnd {
|
||||||
|
int x;
|
||||||
|
int y;
|
||||||
|
|
||||||
|
friend auto operator==(DrawEnd lhs, DrawEnd rhs) -> bool = default;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Constructs an empty buffer holding `charset`, laying out for
|
||||||
|
// `DefaultColumns`.
|
||||||
|
explicit Buffer(Charset charset) : Buffer(DefaultColumns, charset) {}
|
||||||
|
|
||||||
|
// Constructs an empty buffer `columns` wide, which must be in
|
||||||
|
// [1, `MaxColumns`], and whose tabs advance to stops `tab_width` columns
|
||||||
|
// apart.
|
||||||
|
//
|
||||||
|
// The width is what everything drawn into the buffer is laid out for and
|
||||||
|
// checked against, not a starting size. The grid holds it from the start, so
|
||||||
|
// a row is only ever reallocated for something that overhangs it.
|
||||||
|
Buffer(int columns, Charset charset, int tab_width = DefaultTabWidth);
|
||||||
|
|
||||||
|
// Constructs an empty buffer holding `capabilities`'s charset and tab stops,
|
||||||
|
// laying out for its width, or for `DefaultColumns` where it has none.
|
||||||
|
//
|
||||||
|
// Both numbers are clamped rather than checked. They describe a terminal
|
||||||
|
// rather than coming from a caller -- `columns` by way of `COLUMNS`, which
|
||||||
|
// anyone can export as anything -- so a value a grid cannot hold is bad input
|
||||||
|
// rather than a mistake, and the nearest usable one lays out no worse than
|
||||||
|
// the fallback would.
|
||||||
|
explicit Buffer(const Capabilities& capabilities)
|
||||||
|
: Buffer(std::clamp(capabilities.columns.value_or(DefaultColumns), 1,
|
||||||
|
MaxColumns),
|
||||||
|
capabilities.charset,
|
||||||
|
std::clamp(capabilities.tab_width, 1, MaxTabWidth)) {}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
auto width() const -> int { return width_; }
|
||||||
|
|
||||||
|
// Returns the number of rows the grid holds, which is one past the last row
|
||||||
|
// drawn into.
|
||||||
|
auto height() const -> int;
|
||||||
|
|
||||||
|
auto charset() const -> Charset { return metrics_.charset(); }
|
||||||
|
|
||||||
|
// Returns how text is measured for this buffer's charset.
|
||||||
|
//
|
||||||
|
// The buffer lays its cells out with this, so a caller deciding where to put
|
||||||
|
// something asks the same thing the drawing will.
|
||||||
|
auto metrics() const -> Metrics { return metrics_; }
|
||||||
|
|
||||||
|
// Returns where `DrawText` would end for these arguments, without drawing.
|
||||||
|
//
|
||||||
|
// Measuring and drawing walk the text with the same code, differing only in
|
||||||
|
// whether they write a cell, so a layout decision made from this can't
|
||||||
|
// disagree with what drawing then does.
|
||||||
|
//
|
||||||
|
// This is for text that a tab, a newline, or a carriage return makes
|
||||||
|
// positional. Text with none of them is as wide wherever it is drawn, and
|
||||||
|
// `Metrics::Width` answers for it without a buffer to draw into.
|
||||||
|
auto MeasureText(int x, int y, int margin, llvm::StringRef text) const
|
||||||
|
-> DrawEnd;
|
||||||
|
|
||||||
|
// Returns where the `DrawText` taking no margin would end, which draws `text`
|
||||||
|
// as text of its own beginning at (x, y).
|
||||||
|
auto MeasureText(int x, int y, llvm::StringRef text) const -> DrawEnd {
|
||||||
|
return MeasureText(x, y, x, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns where `DrawWrappedText` would end for these arguments, without
|
||||||
|
// drawing.
|
||||||
|
//
|
||||||
|
// The block and the origin are checked as drawing checks them, so measuring
|
||||||
|
// answers only for arguments drawing would accept.
|
||||||
|
auto MeasureWrappedText(int x, int y, int margin, int max_width,
|
||||||
|
llvm::StringRef text) const -> DrawEnd;
|
||||||
|
|
||||||
|
// Returns the fewest columns `text` wraps into without overhanging them,
|
||||||
|
// which is the width of its widest word since wrapping never breaks one.
|
||||||
|
//
|
||||||
|
// Wrapping into fewer columns still draws everything; the excess overhangs.
|
||||||
|
// 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.
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
auto DrawCodePoint(int x, int y, char32_t code_point, const Style& style)
|
||||||
|
-> DrawEnd;
|
||||||
|
|
||||||
|
// Draws a horizontal line across `length` columns starting at (x, y).
|
||||||
|
//
|
||||||
|
// By default the line runs between the centers of its first and last cells,
|
||||||
|
// which is what a line connecting two things is: `DrawBox` draws its four
|
||||||
|
// sides this way, and each pair meets at a corner. `LineEnd::Edge` instead
|
||||||
|
// runs that end out through the side of its cell, which is what a line
|
||||||
|
// bounding `length` whole columns of something is, and what makes a line
|
||||||
|
// meeting it there a tee. A line of one column between two centers is a
|
||||||
|
// point, and is drawn as one.
|
||||||
|
//
|
||||||
|
// Lines join wherever they overlap: a cell records which directions lines
|
||||||
|
// leave it in, and its glyph follows from those bits alone, so crossings,
|
||||||
|
// corners, and tees all appear without being asked for and whatever order
|
||||||
|
// the lines were drawn in. This is the only way to produce a junction, and
|
||||||
|
// it suffices because a junction in real line art always has the lines that
|
||||||
|
// imply it running through it. Only line drawing records directions, so text
|
||||||
|
// containing `-` or `+` is never redrawn as line art.
|
||||||
|
//
|
||||||
|
// A cell's style is whatever was drawn there last, so crossing lines of
|
||||||
|
// different styles do depend on order.
|
||||||
|
auto DrawHorizontalLine(int x, int y, int length, const Style& style,
|
||||||
|
LineEnd start = LineEnd::Center,
|
||||||
|
LineEnd end = LineEnd::Center) -> DrawEnd;
|
||||||
|
|
||||||
|
// Draws a vertical line down `length` rows starting at (x, y), with the same
|
||||||
|
// meaning for its ends. Returns the row after it, in the column it ran down.
|
||||||
|
auto DrawVerticalLine(int x, int y, int length, const Style& style,
|
||||||
|
LineEnd start = LineEnd::Center,
|
||||||
|
LineEnd end = LineEnd::Center) -> DrawEnd;
|
||||||
|
|
||||||
|
// Draws the outline of a box with its top-left corner at (x, y).
|
||||||
|
//
|
||||||
|
// Each side runs between the centers of the cells it ends in, so the four
|
||||||
|
// corners come out of the sides meeting there. A box with no interior is
|
||||||
|
// then the single line that bounds it, and one with no extent in either
|
||||||
|
// direction is a point, without either being a case of its own.
|
||||||
|
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`.
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
//
|
||||||
|
// 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,
|
||||||
|
// with stops measured from `margin` so that a quoted source line keeps the
|
||||||
|
// tab alignment it had in the file wherever the quote is placed. Returns
|
||||||
|
// where it ended, which for text with a newline in it is on a later row than
|
||||||
|
// it started.
|
||||||
|
//
|
||||||
|
// The margin is what lets text with newlines in it be drawn as differently
|
||||||
|
// styled spans, each starting where the last ended and all naming the same
|
||||||
|
// margin, the way `DrawWrappedText` does for a block: a newline in the middle
|
||||||
|
// of such a run returns to the text's own left edge rather than to wherever
|
||||||
|
// the span it fell in happened to start.
|
||||||
|
auto DrawText(int x, int y, int margin, llvm::StringRef text,
|
||||||
|
const Style& style) -> DrawEnd;
|
||||||
|
|
||||||
|
// Draws `text` as text of its own beginning at (x, y), which is then both
|
||||||
|
// where it starts and the margin its later rows return to.
|
||||||
|
auto DrawText(int x, int y, llvm::StringRef text, const Style& style)
|
||||||
|
-> DrawEnd {
|
||||||
|
return DrawText(x, y, x, text, style);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draws `text` starting at (x, y), into the block of `max_width` columns
|
||||||
|
// beginning at `margin`.
|
||||||
|
//
|
||||||
|
// The block must lie within `columns()` and `x` within the block, so
|
||||||
|
// `0 <= margin <= x < margin + max_width <= columns()`. A block is a division
|
||||||
|
// of the width rather than something that can exceed it: what a caller wants
|
||||||
|
// when it has nothing to divide is `max_width` of `columns() - margin`, the
|
||||||
|
// whole of what is left.
|
||||||
|
//
|
||||||
|
// The block is what the text wraps within, and (x, y) is only where this run
|
||||||
|
// of it starts: rows after the first begin at `margin`, and how much room a
|
||||||
|
// row has is measured from there. A block whose spans are styled differently
|
||||||
|
// is drawn as one call per span, each starting where the last ended and all
|
||||||
|
// naming the same margin and width. Passing `x` as the margin draws a block
|
||||||
|
// in one call.
|
||||||
|
//
|
||||||
|
// Wrapping breaks at ASCII spaces, tabs, and carriage returns, and only
|
||||||
|
// there. A word here is whatever lies between two of them, so a URL is one
|
||||||
|
// word, and one too long for a row of its own is moved down to one and then
|
||||||
|
// overhangs it rather than being broken.
|
||||||
|
//
|
||||||
|
// Whitespace stops at the block's edge rather than running past it, so the
|
||||||
|
// spaces between two words stay on the row the first of them ended and the
|
||||||
|
// row the second wraps onto begins at the margin. Spaces the text opens with,
|
||||||
|
// or that follow a newline in it, are kept as they are, since those are
|
||||||
|
// indentation the caller wrote.
|
||||||
|
//
|
||||||
|
// Newlines are breaks the caller already made, and are kept as they are:
|
||||||
|
// wrapping only adds breaks to the text it is given. They break the line as a
|
||||||
|
// wrap does, continuing at `margin` on the next row, and carriage returns are
|
||||||
|
// dropped so that CRLF endings break exactly once.
|
||||||
|
//
|
||||||
|
// A tab is both a break opportunity and a jump to the next tab stop, with
|
||||||
|
// stops measured from `margin` rather than from `x`. The margin is the one
|
||||||
|
// column every row of the block begins at, so the stops are the same on each
|
||||||
|
// of them and a tabbed column stays a column however the text wraps; stops
|
||||||
|
// from `x` would move with the span that happened to be drawn first. A tab
|
||||||
|
// that would reach past the block stops at its edge, like the spaces do,
|
||||||
|
// leaving the word after it to wrap.
|
||||||
|
//
|
||||||
|
// `DrawText` is the way to draw text that should not wrap at all, and differs
|
||||||
|
// in more than that: it keeps every space, and returns to the margin on a
|
||||||
|
// carriage return rather than dropping it.
|
||||||
|
//
|
||||||
|
// Returns where it ended.
|
||||||
|
//
|
||||||
|
// TODO: There is no mode that reflows, treating the newlines in `text` as
|
||||||
|
// breaks to be chosen again rather than kept. Text that arrives wrapped to
|
||||||
|
// some other width keeps that wrapping, which is wrong for it wherever that
|
||||||
|
// width isn't the one it is being drawn into. Add one when there is a caller
|
||||||
|
// with such text, since which breaks a reflow may discard -- every newline,
|
||||||
|
// or only those a previous wrapping introduced -- is a question about where
|
||||||
|
// that text came from.
|
||||||
|
auto DrawWrappedText(int x, int y, int margin, int max_width,
|
||||||
|
llvm::StringRef text, const Style& style) -> DrawEnd;
|
||||||
|
|
||||||
|
// Renders the grid, appending the bytes that draw it to `out`.
|
||||||
|
//
|
||||||
|
// Each row ends in a newline, with trailing blank cells dropped so output
|
||||||
|
// carries no invisible padding. The rendering ends with the style turned off
|
||||||
|
// so nothing bleeds into what is printed next, and a style that paints blank
|
||||||
|
// cells is turned off at each row's end so a background does not run to the
|
||||||
|
// right edge. Color is chosen here rather than at construction because it
|
||||||
|
// affects only how cells are serialized, while the charset decides how
|
||||||
|
// content is laid out into them.
|
||||||
|
auto Render(OutputBufferRef out, ColorMode mode) const -> void;
|
||||||
|
|
||||||
|
// Renders the grid and writes it to `file`.
|
||||||
|
//
|
||||||
|
// The whole grid goes out in one `write` where the destination accepts it,
|
||||||
|
// which is what gives the output whatever atomicity the descriptor offers
|
||||||
|
// against other writers: a terminal or a pipe interleaves at write
|
||||||
|
// boundaries, so one call per rendered buffer is the most that can be had
|
||||||
|
// without a lock.
|
||||||
|
auto WriteTo(Filesystem::WriteFileRef file, ColorMode mode) const
|
||||||
|
-> ErrorOr<Success, Filesystem::FdError>;
|
||||||
|
|
||||||
|
private:
|
||||||
|
// The directions in which drawn lines leave a cell, and whether the cell
|
||||||
|
// holds line art at all. A cell's glyph is a function of the directions
|
||||||
|
// alone.
|
||||||
|
enum LineDirection : uint8_t {
|
||||||
|
LineLeft = 1 << 0,
|
||||||
|
LineRight = 1 << 1,
|
||||||
|
LineUp = 1 << 2,
|
||||||
|
LineDown = 1 << 3,
|
||||||
|
LineDirections = 0b1111,
|
||||||
|
// Set on every cell line drawing writes. A cell can hold line art and no
|
||||||
|
// directions -- a line between one center and itself is a point -- and
|
||||||
|
// without this such a cell would be indistinguishable from one holding
|
||||||
|
// text, so nothing drawn later would join it.
|
||||||
|
LineCell = 1 << 4,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Cell {
|
||||||
|
// The code point rendered here. For a cell with `lines` set, this is
|
||||||
|
// derived from those bits and the charset.
|
||||||
|
char32_t code_point = ' ';
|
||||||
|
|
||||||
|
Style style;
|
||||||
|
|
||||||
|
// Which directions drawn lines leave this cell in, with `LineCell` set,
|
||||||
|
// or zero for a cell holding text.
|
||||||
|
uint8_t lines = 0;
|
||||||
|
|
||||||
|
// Whether this cell is the right half of a double-width character, and so
|
||||||
|
// renders nothing of its own.
|
||||||
|
bool is_continuation = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Checks that `text` is short enough to measure without overflowing a column.
|
||||||
|
static auto CheckTextSize(llvm::StringRef text) -> void {
|
||||||
|
CARBON_CHECK(text.size() <= MaxTextBytes,
|
||||||
|
"Laying out {0} bytes of text is past the {1} one operation "
|
||||||
|
"handles.",
|
||||||
|
text.size(), MaxTextBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto CellIndex(int x, int y) const -> int { return y * width_ + x; }
|
||||||
|
auto CellAt(int x, int y) -> Cell& { return cells_[CellIndex(x, y)]; }
|
||||||
|
auto CellAt(int x, int y) const -> const Cell& {
|
||||||
|
return cells_[CellIndex(x, y)];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks that (x, y) is somewhere a drawing may start.
|
||||||
|
//
|
||||||
|
// 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 CheckOrigin(int x, int y) const -> void {
|
||||||
|
CARBON_CHECK(
|
||||||
|
x >= 0 && x < columns_ && y >= 0 && y < MaxRows,
|
||||||
|
"Drawing at ({0}, {1}) is outside the {2} columns and {3} rows "
|
||||||
|
"a buffer covers.",
|
||||||
|
x, y, columns_, MaxRows);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Places `code_point` at (x, y) without checking it against the target width
|
||||||
|
// or `MaxRows`, which text reaches on its own by overhanging or by carrying
|
||||||
|
// newlines. Past either, nothing is drawn and the column still advances. The
|
||||||
|
// coordinates must be non-negative, which follows from the origin the walk
|
||||||
|
// was checked at.
|
||||||
|
auto PlaceCodePoint(int x, int y, char32_t code_point, const Style& style)
|
||||||
|
-> int;
|
||||||
|
|
||||||
|
// The walks behind the text operations, over which drawing and measuring are
|
||||||
|
// the same code. `place` is called with each code point and where it goes,
|
||||||
|
// and returns the column after it: `PlaceCodePoint` when drawing, and the
|
||||||
|
// width alone when measuring.
|
||||||
|
template <typename PlaceFn>
|
||||||
|
auto WalkText(int x, int y, int margin, llvm::StringRef text,
|
||||||
|
PlaceFn place) const -> DrawEnd;
|
||||||
|
template <typename PlaceFn>
|
||||||
|
auto WalkWrappedText(int x, int y, int margin, int max_width,
|
||||||
|
llvm::StringRef text, PlaceFn place) const -> DrawEnd;
|
||||||
|
|
||||||
|
// Adds rows until row `y` exists.
|
||||||
|
auto EnsureRow(int y) -> void;
|
||||||
|
|
||||||
|
// Widens the grid until column `x` exists, reflowing the rows it already
|
||||||
|
// holds, which are stored back to back. Only something overhanging the target
|
||||||
|
// width reaches past it, so this runs for nothing else.
|
||||||
|
auto EnsureColumn(int x) -> void;
|
||||||
|
|
||||||
|
// Resets the cells in row `y` spanning columns [x, x + width), along with
|
||||||
|
// either half of a double-width character that straddles the range's edges.
|
||||||
|
auto ClearCells(int x, int y, int width) -> void;
|
||||||
|
|
||||||
|
// Appends `code_point` to the marks rendered with the cell before column `x`.
|
||||||
|
auto AttachCombiningMark(int x, int y, char32_t code_point) -> void;
|
||||||
|
|
||||||
|
// Adds `directions` to the lines through (x, y) and updates its glyph.
|
||||||
|
auto DrawLine(int x, int y, uint8_t directions, const Style& style) -> void;
|
||||||
|
|
||||||
|
// Returns the last column in row `y` that renders anything under `mode`, or
|
||||||
|
// -1 when the row renders nothing.
|
||||||
|
auto LastVisibleColumn(int y, ColorMode mode) const -> int;
|
||||||
|
|
||||||
|
// The width laid out for, and the width the grid holds. They differ only
|
||||||
|
// where something overhung the first.
|
||||||
|
int columns_;
|
||||||
|
int width_;
|
||||||
|
|
||||||
|
int tab_width_;
|
||||||
|
Metrics metrics_;
|
||||||
|
|
||||||
|
llvm::SmallVector<Cell, 0> cells_;
|
||||||
|
|
||||||
|
// Combining marks, as UTF-8, for the few cells that have any, keyed by cell
|
||||||
|
// index. Kept out of `Cell` so that the common case of no marks costs
|
||||||
|
// nothing per cell. Always empty under `Charset::Ascii`.
|
||||||
|
llvm::DenseMap<int, std::string> combining_marks_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Carbon::Terminal
|
||||||
|
|
||||||
|
#endif // CARBON_COMMON_TERMINAL_BUFFER_H_
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,216 @@
|
|||||||
|
// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
|
||||||
|
// Exceptions. See /LICENSE for license information.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
||||||
|
|
||||||
|
#include "common/terminal/capabilities.h"
|
||||||
|
|
||||||
|
#include <sys/ioctl.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
|
#include "llvm/ADT/StringExtras.h"
|
||||||
|
#include "llvm/ADT/StringSwitch.h"
|
||||||
|
|
||||||
|
namespace Carbon::Terminal {
|
||||||
|
|
||||||
|
// Returns the value of `name` in the process environment, empty when unset.
|
||||||
|
static auto GetEnv(const char* name) -> llvm::StringRef {
|
||||||
|
const char* value = std::getenv(name);
|
||||||
|
return value ? llvm::StringRef(value) : llvm::StringRef();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ColorEnvironment::FromProcess() -> ColorEnvironment {
|
||||||
|
return {.no_color = GetEnv("NO_COLOR"),
|
||||||
|
.clicolor_force = GetEnv("CLICOLOR_FORCE"),
|
||||||
|
.force_color = GetEnv("FORCE_COLOR"),
|
||||||
|
.clicolor = GetEnv("CLICOLOR"),
|
||||||
|
.colorterm = GetEnv("COLORTERM"),
|
||||||
|
.term_program = GetEnv("TERM_PROGRAM"),
|
||||||
|
.term = GetEnv("TERM")};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns whether the environment and the stream call for color, ignoring any
|
||||||
|
// explicit preference. See `ChooseColorMode` for the precedence this
|
||||||
|
// implements and where it comes from.
|
||||||
|
static auto EnvironmentEnablesColor(const ColorEnvironment& env,
|
||||||
|
bool is_terminal) -> bool {
|
||||||
|
if (!env.no_color.empty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// The forcing variables use `0` to decline to force, and `FORCE_COLOR` takes
|
||||||
|
// it further as a request to disable.
|
||||||
|
if (env.force_color == "0") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!env.force_color.empty() ||
|
||||||
|
(!env.clicolor_force.empty() && env.clicolor_force != "0")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (env.clicolor == "0") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!is_terminal) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// `dumb` says outright that escape sequences won't render, which outranks
|
||||||
|
// anything below claiming they will.
|
||||||
|
if (env.term == "dumb") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Any of these identifies the terminal as something that renders escapes. A
|
||||||
|
// terminal that none of them describe can't be assumed to.
|
||||||
|
//
|
||||||
|
// `COLORTERM` and `TERM_PROGRAM` stand on their own rather than refining
|
||||||
|
// `TERM`: `TERM` names a terminfo entry, while these name the emulator and
|
||||||
|
// the color it handles.
|
||||||
|
return !env.term.empty() || !env.colorterm.empty() ||
|
||||||
|
!env.term_program.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the richest color escapes the terminal is believed to accept.
|
||||||
|
//
|
||||||
|
// Every signal here is a heuristic: there is no way to ask a terminal what it
|
||||||
|
// supports without writing to it and parsing a reply, which would be far too
|
||||||
|
// invasive for a compiler. Guessing too high garbles color on a terminal that
|
||||||
|
// can't keep up, and guessing too low only makes output plainer, so unknown
|
||||||
|
// terminals get the conservative answer.
|
||||||
|
static auto DetectColorDepth(const ColorEnvironment& env) -> ColorMode {
|
||||||
|
// `FORCE_COLOR`'s levels name a depth outright.
|
||||||
|
if (auto mode = llvm::StringSwitch<std::optional<ColorMode>>(env.force_color)
|
||||||
|
.Case("1", ColorMode::Ansi16)
|
||||||
|
.Case("2", ColorMode::Ansi256)
|
||||||
|
.Case("3", ColorMode::Truecolor)
|
||||||
|
.Default(std::nullopt)) {
|
||||||
|
return *mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The convention documented at
|
||||||
|
// https://github.com/termstandard/colors#checking-for-colorterm.
|
||||||
|
if (env.colorterm == "truecolor" || env.colorterm == "24bit") {
|
||||||
|
return ColorMode::Truecolor;
|
||||||
|
}
|
||||||
|
|
||||||
|
// `TERM_PROGRAM` identifies the emulator regardless of how `TERM` is set,
|
||||||
|
// which matters because several of these ship a conservative `TERM` while
|
||||||
|
// rendering far more than it claims.
|
||||||
|
{
|
||||||
|
if (auto mode =
|
||||||
|
llvm::StringSwitch<std::optional<ColorMode>>(env.term_program)
|
||||||
|
.Case("vscode", ColorMode::Truecolor)
|
||||||
|
.Case("iTerm.app", ColorMode::Truecolor)
|
||||||
|
.Case("WarpTerminal", ColorMode::Truecolor)
|
||||||
|
.Case("Hyper", ColorMode::Truecolor)
|
||||||
|
.Case("Tabby", ColorMode::Truecolor)
|
||||||
|
.Case("Terminus", ColorMode::Truecolor)
|
||||||
|
// Apple's Terminal renders only the 256-color palette.
|
||||||
|
.Case("Apple_Terminal", ColorMode::Ansi256)
|
||||||
|
.Default(std::nullopt)) {
|
||||||
|
return *mode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The enumerated terminals that stand in for a terminfo lookup.
|
||||||
|
{
|
||||||
|
if (auto mode = llvm::StringSwitch<std::optional<ColorMode>>(env.term)
|
||||||
|
.Case("xterm-kitty", ColorMode::Truecolor)
|
||||||
|
.Case("alacritty", ColorMode::Truecolor)
|
||||||
|
.Case("wezterm", ColorMode::Truecolor)
|
||||||
|
.Case("ghostty", ColorMode::Truecolor)
|
||||||
|
.StartsWith("foot", ColorMode::Truecolor)
|
||||||
|
.StartsWith("contour", ColorMode::Truecolor)
|
||||||
|
.StartsWith("vte", ColorMode::Truecolor)
|
||||||
|
.EndsWith("-direct", ColorMode::Truecolor)
|
||||||
|
.EndsWith("-truecolor", ColorMode::Truecolor)
|
||||||
|
.EndsWith("-256color", ColorMode::Ansi256)
|
||||||
|
.EndsWith("-256", ColorMode::Ansi256)
|
||||||
|
.Default(std::nullopt)) {
|
||||||
|
return *mode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Color is called for, but nothing said how much of it works.
|
||||||
|
return ColorMode::Ansi16;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ChooseColorMode(Preference preference, const ColorEnvironment& env,
|
||||||
|
bool is_terminal) -> ColorMode {
|
||||||
|
switch (preference) {
|
||||||
|
case Preference::Never:
|
||||||
|
return ColorMode::NoColor;
|
||||||
|
case Preference::Always:
|
||||||
|
break;
|
||||||
|
case Preference::Auto:
|
||||||
|
if (!EnvironmentEnablesColor(env, is_terminal)) {
|
||||||
|
return ColorMode::NoColor;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return DetectColorDepth(env);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ChooseCharset(Preference preference, llvm::StringRef locale) -> Charset {
|
||||||
|
switch (preference) {
|
||||||
|
case Preference::Never:
|
||||||
|
return Charset::Ascii;
|
||||||
|
case Preference::Always:
|
||||||
|
return Charset::Utf8;
|
||||||
|
case Preference::Auto:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Locale names spell the encoding several ways: `en_US.UTF-8`, `C.utf8`, and
|
||||||
|
// bare `UTF-8` all appear in the wild.
|
||||||
|
return locale.contains_insensitive("utf-8") ||
|
||||||
|
locale.contains_insensitive("utf8")
|
||||||
|
? Charset::Utf8
|
||||||
|
: Charset::Ascii;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the locale that determines the terminal's character encoding,
|
||||||
|
// following the precedence POSIX defines for `LC_CTYPE`.
|
||||||
|
static auto GetLocale() -> llvm::StringRef {
|
||||||
|
for (const char* name : {"LC_ALL", "LC_CTYPE", "LANG"}) {
|
||||||
|
if (llvm::StringRef value = GetEnv(name); !value.empty()) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the terminal's width in columns, or nullopt when there is nothing to
|
||||||
|
// ask.
|
||||||
|
//
|
||||||
|
// `COLUMNS` comes first: when it is exported, the user has deliberately
|
||||||
|
// overridden the real width.
|
||||||
|
static auto GetColumns(int fd) -> std::optional<int> {
|
||||||
|
int columns = 0;
|
||||||
|
if (llvm::to_integer(GetEnv("COLUMNS"), columns) && columns > 0) {
|
||||||
|
return columns;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct winsize size = {};
|
||||||
|
if (ioctl(fd, TIOCGWINSZ, &size) == 0 && size.ws_col > 0) {
|
||||||
|
return size.ws_col;
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Capabilities::Detect(Filesystem::WriteFileRef file,
|
||||||
|
Preferences preferences) -> Capabilities {
|
||||||
|
int fd = file.unix_fd();
|
||||||
|
|
||||||
|
Capabilities capabilities;
|
||||||
|
capabilities.is_terminal = isatty(fd) != 0;
|
||||||
|
capabilities.color_mode =
|
||||||
|
ChooseColorMode(preferences.color, ColorEnvironment::FromProcess(),
|
||||||
|
capabilities.is_terminal);
|
||||||
|
capabilities.charset = ChooseCharset(preferences.utf8, GetLocale());
|
||||||
|
capabilities.columns = GetColumns(fd);
|
||||||
|
|
||||||
|
return capabilities;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Carbon::Terminal
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
|
||||||
|
// Exceptions. See /LICENSE for license information.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
||||||
|
|
||||||
|
#ifndef CARBON_COMMON_TERMINAL_CAPABILITIES_H_
|
||||||
|
#define CARBON_COMMON_TERMINAL_CAPABILITIES_H_
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
#include "common/filesystem.h"
|
||||||
|
#include "common/terminal/color.h"
|
||||||
|
#include "llvm/ADT/StringRef.h"
|
||||||
|
|
||||||
|
namespace Carbon::Terminal {
|
||||||
|
|
||||||
|
// The encoding the terminal decodes output with.
|
||||||
|
//
|
||||||
|
// This decides far more than which characters can be drawn: rendering has to
|
||||||
|
// count the columns a run of bytes will occupy, and that count only follows
|
||||||
|
// from the code points those bytes encode if the terminal agrees about the
|
||||||
|
// encoding. Disagreeing misaligns the entire line rather than drawing a single
|
||||||
|
// character wrong.
|
||||||
|
//
|
||||||
|
// No other encoding is modeled. A terminal decoding something else, an ISO 8859
|
||||||
|
// part for example, is treated as `Ascii`. That is correct output for any of
|
||||||
|
// them, as they all encode printable ASCII as itself, and rendering in one
|
||||||
|
// natively would mean carrying its conversion and column-width tables to gain
|
||||||
|
// nothing but nicer line drawing, which `Ascii` already has a fallback for. So
|
||||||
|
// `Utf8` is used only where the environment says outright that the terminal
|
||||||
|
// decodes UTF-8, and `Ascii` does no UTF-8 processing at all.
|
||||||
|
enum class Charset : int8_t {
|
||||||
|
// Every byte is one column, and lines are drawn from `-`, `|`, and `+`.
|
||||||
|
//
|
||||||
|
// Bytes outside printable ASCII are replaced rather than passed through,
|
||||||
|
// because a terminal decoding some single-byte encoding will render them as
|
||||||
|
// something, and there is no way to know what.
|
||||||
|
Ascii,
|
||||||
|
// Bytes are decoded as UTF-8, giving double-width characters two columns and
|
||||||
|
// combining marks none, and lines are drawn with box-drawing characters.
|
||||||
|
Utf8,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Whether to use one of the terminal features detection decides about, where
|
||||||
|
// an explicit request overrides what detection would conclude.
|
||||||
|
//
|
||||||
|
// This is the tri-state that a `--color=never` style flag parses into. It says
|
||||||
|
// nothing about which feature is being requested; `Preferences` holds one of
|
||||||
|
// these per feature.
|
||||||
|
enum class Preference : int8_t {
|
||||||
|
// Decide from the environment and the stream.
|
||||||
|
Auto,
|
||||||
|
// Never use the feature, whatever the environment says.
|
||||||
|
Never,
|
||||||
|
// Use the feature even when the stream isn't a terminal. This is what a
|
||||||
|
// caller wants when piping into a pager, capturing output for later replay,
|
||||||
|
// or writing a test.
|
||||||
|
Always,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The environment variables that control whether and how color is used.
|
||||||
|
//
|
||||||
|
// An unset variable and one set to the empty string mean the same thing
|
||||||
|
// throughout: no opinion. Values point into the process environment and are
|
||||||
|
// invalidated by anything that modifies it.
|
||||||
|
struct ColorEnvironment {
|
||||||
|
// Reads the variables from the process environment.
|
||||||
|
static auto FromProcess() -> ColorEnvironment;
|
||||||
|
|
||||||
|
llvm::StringRef no_color;
|
||||||
|
llvm::StringRef clicolor_force;
|
||||||
|
llvm::StringRef force_color;
|
||||||
|
llvm::StringRef clicolor;
|
||||||
|
llvm::StringRef colorterm;
|
||||||
|
llvm::StringRef term_program;
|
||||||
|
llvm::StringRef term;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Returns the color mode to render with.
|
||||||
|
//
|
||||||
|
// Whether to use color at all is decided first, from highest priority to
|
||||||
|
// lowest:
|
||||||
|
//
|
||||||
|
// - An explicit `Never` or `Always` preference.
|
||||||
|
// - `NO_COLOR` set to any non-empty value disables color: see
|
||||||
|
// https://no-color.org.
|
||||||
|
// - `FORCE_COLOR=0` disables color, following the Node convention.
|
||||||
|
// - Any other non-empty `FORCE_COLOR`, or a non-empty `CLICOLOR_FORCE` other
|
||||||
|
// than `0`, enables color even when the stream isn't a terminal.
|
||||||
|
// - `CLICOLOR=0` disables color.
|
||||||
|
// - `TERM=dumb` disables color, being an explicit statement that escape
|
||||||
|
// sequences won't render.
|
||||||
|
// - Otherwise color is used only when the stream is a terminal and something
|
||||||
|
// identifies that terminal: `TERM` set to anything else, or `COLORTERM` or
|
||||||
|
// `TERM_PROGRAM` set at all. The latter two stand on their own rather than
|
||||||
|
// refining `TERM`, because the emulator sets them itself and they are
|
||||||
|
// specifically about color, while `TERM` is left unset by anything not
|
||||||
|
// launched from a shell.
|
||||||
|
//
|
||||||
|
// How much color to use is then guessed from `FORCE_COLOR`'s level,
|
||||||
|
// `COLORTERM`, `TERM_PROGRAM`, and `TERM`, falling back to `Ansi16` when color
|
||||||
|
// is called for but nothing says how much of it works. Apart from
|
||||||
|
// `FORCE_COLOR`, which is a request rather than a description, none of these
|
||||||
|
// enable color on their own, so a rich `COLORTERM` inherited by a redirected
|
||||||
|
// stream can't put escape sequences into it.
|
||||||
|
//
|
||||||
|
// Depth comes from enumerating known terminals rather than from terminfo,
|
||||||
|
// which trades a list to maintain here for not depending on databases that are
|
||||||
|
// routinely absent from the containers and CI images this runs in. An
|
||||||
|
// unrecognized terminal gets the conservative answer.
|
||||||
|
//
|
||||||
|
// This is separated from `Capabilities::Detect` so that the policy can be
|
||||||
|
// tested without touching the process environment.
|
||||||
|
auto ChooseColorMode(Preference preference, const ColorEnvironment& env,
|
||||||
|
bool is_terminal) -> ColorMode;
|
||||||
|
|
||||||
|
// Returns the encoding to render with, where `locale` is the value of the
|
||||||
|
// first set variable among `LC_ALL`, `LC_CTYPE`, and `LANG`.
|
||||||
|
//
|
||||||
|
// Only a locale that names UTF-8 gets `Utf8`. Guessing wrong in that direction
|
||||||
|
// costs alignment on every line that isn't pure ASCII, while guessing wrong
|
||||||
|
// the other way only makes output plainer.
|
||||||
|
auto ChooseCharset(Preference preference, llvm::StringRef locale) -> Charset;
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// as if nothing bounded it, which a terminal then wraps at column zero --
|
||||||
|
// breaking every indent and gutter it was given, and in the middle of whatever
|
||||||
|
// word it lands on. The cost of guessing is asymmetric: a viewer wider than
|
||||||
|
// this sees slack on the right, while one narrower sees the wrapping done
|
||||||
|
// twice, ours and then its own.
|
||||||
|
//
|
||||||
|
// Eighty is the traditional terminal width, and narrower ones are rare enough
|
||||||
|
// that fitting them would cost more in wasted width everywhere else.
|
||||||
|
inline constexpr int DefaultColumns = 80;
|
||||||
|
|
||||||
|
// The columns between tab stops, absent anything saying otherwise.
|
||||||
|
//
|
||||||
|
// Eight is the interval terminfo records as `it#8` for all but a handful of
|
||||||
|
// legacy entries. Nothing measures a terminal's stops, so unlike its width this
|
||||||
|
// stands in for no measurement: `Capabilities` carries it as a plain value
|
||||||
|
// rather than as one a caller can tell apart from an absence.
|
||||||
|
inline constexpr int DefaultTabWidth = 8;
|
||||||
|
|
||||||
|
// What the terminal behind a stream can render, and how wide it is.
|
||||||
|
//
|
||||||
|
// Detect this once per stream at startup and pass it down; the fields come from
|
||||||
|
// environment queries and system calls that shouldn't be repeated per
|
||||||
|
// diagnostic.
|
||||||
|
struct Capabilities {
|
||||||
|
// Detects the capabilities of the terminal behind `file`, honoring
|
||||||
|
// `preferences`.
|
||||||
|
//
|
||||||
|
// Detection reads the descriptor directly, because `isatty` and `TIOCGWINSZ`
|
||||||
|
// 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.
|
||||||
|
static auto Detect(Filesystem::WriteFileRef file,
|
||||||
|
Preferences preferences = {}) -> Capabilities;
|
||||||
|
|
||||||
|
// The richest color escapes the terminal is believed to understand.
|
||||||
|
ColorMode color_mode = ColorMode::NoColor;
|
||||||
|
|
||||||
|
// The encoding the terminal decodes output with.
|
||||||
|
Charset charset = Charset::Ascii;
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
// The terminal's width, or none when nothing says how wide the output is.
|
||||||
|
// Positive whenever it is set, so layout can divide by it freely.
|
||||||
|
//
|
||||||
|
// This says what was measured, and nothing is invented to fill it in: an
|
||||||
|
// absence is a real answer about a pipe nobody described. Laying out still
|
||||||
|
// needs a width, and `DefaultColumns` is what a layout falls back to, so
|
||||||
|
// whether this is set decides whether output is fitted to the terminal in
|
||||||
|
// front of it or to a width chosen to be safe wherever it ends up.
|
||||||
|
std::optional<int> columns;
|
||||||
|
|
||||||
|
// The columns between the terminal's tab stops, which is what a tab in text
|
||||||
|
// advances to the next of.
|
||||||
|
//
|
||||||
|
// TODO: Nothing sets this away from `DefaultTabWidth`. A terminal's stops are
|
||||||
|
// mutable at runtime -- `hts` sets one and `tbc` clears them -- so the only
|
||||||
|
// report of the live ones is `DECRQPSR`, which few emulators outside `xterm`
|
||||||
|
// answer, or a `DSR-CPR` round trip after writing a tab, which nearly all do.
|
||||||
|
// Either means putting the descriptor in raw mode and reading a reply with a
|
||||||
|
// timeout. Add it when a terminal that disagrees with eight is worth that.
|
||||||
|
int tab_width = DefaultTabWidth;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Carbon::Terminal
|
||||||
|
|
||||||
|
#endif // CARBON_COMMON_TERMINAL_CAPABILITIES_H_
|
||||||
@@ -0,0 +1,287 @@
|
|||||||
|
// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
|
||||||
|
// Exceptions. See /LICENSE for license information.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
||||||
|
|
||||||
|
#include "common/terminal/capabilities.h"
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include "common/filesystem.h"
|
||||||
|
|
||||||
|
namespace Carbon::Terminal {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Detection policy is a pure function of the environment and whether the
|
||||||
|
// stream is a terminal, so these tests build the environment directly instead
|
||||||
|
// of mutating the process environment, which would leak between tests and race
|
||||||
|
// with anything else running.
|
||||||
|
auto OnTerminal(const ColorEnvironment& env) -> ColorMode {
|
||||||
|
return ChooseColorMode(Preference::Auto, env, /*is_terminal=*/true);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto OffTerminal(const ColorEnvironment& env) -> ColorMode {
|
||||||
|
return ChooseColorMode(Preference::Auto, env, /*is_terminal=*/false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A terminal that supports color, for tests varying one other variable.
|
||||||
|
auto ColorTerminal() -> ColorEnvironment { return {.term = "xterm-256color"}; }
|
||||||
|
|
||||||
|
TEST(CapabilitiesTest, ColorNeedsATerminal) {
|
||||||
|
EXPECT_EQ(OnTerminal(ColorTerminal()), ColorMode::Ansi256);
|
||||||
|
|
||||||
|
// Writing to a file or a pipe must stay plain, or every redirected build log
|
||||||
|
// fills with escape sequences.
|
||||||
|
EXPECT_EQ(OffTerminal(ColorTerminal()), ColorMode::NoColor);
|
||||||
|
|
||||||
|
// A terminal that nothing says anything about can't be assumed to render
|
||||||
|
// escapes.
|
||||||
|
EXPECT_EQ(OnTerminal({}), ColorMode::NoColor);
|
||||||
|
EXPECT_EQ(OnTerminal({.term = ""}), ColorMode::NoColor);
|
||||||
|
EXPECT_EQ(OnTerminal({.term = "dumb"}), ColorMode::NoColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(CapabilitiesTest, ColorFromTheEmulatorWithoutTerm) {
|
||||||
|
// `TERM` is unset for anything not launched from a shell, but the emulator
|
||||||
|
// sets `COLORTERM` and `TERM_PROGRAM` itself, and both are specifically
|
||||||
|
// about color. Either one identifies the terminal on its own, at whatever
|
||||||
|
// depth it names.
|
||||||
|
EXPECT_EQ(OnTerminal({.colorterm = "truecolor"}), ColorMode::Truecolor);
|
||||||
|
EXPECT_EQ(OnTerminal({.colorterm = "yes"}), ColorMode::Ansi16);
|
||||||
|
EXPECT_EQ(OnTerminal({.term_program = "vscode"}), ColorMode::Truecolor);
|
||||||
|
EXPECT_EQ(OnTerminal({.term_program = "unknown"}), ColorMode::Ansi16);
|
||||||
|
|
||||||
|
// An empty value says nothing at all.
|
||||||
|
EXPECT_EQ(OnTerminal({.colorterm = "", .term_program = ""}),
|
||||||
|
ColorMode::NoColor);
|
||||||
|
|
||||||
|
// `dumb` outranks them: it states outright that escapes won't render.
|
||||||
|
EXPECT_EQ(OnTerminal({.colorterm = "truecolor", .term = "dumb"}),
|
||||||
|
ColorMode::NoColor);
|
||||||
|
|
||||||
|
// And none of them enable color off a terminal.
|
||||||
|
EXPECT_EQ(OffTerminal({.colorterm = "truecolor"}), ColorMode::NoColor);
|
||||||
|
EXPECT_EQ(OffTerminal({.term_program = "vscode"}), ColorMode::NoColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(CapabilitiesTest, ExplicitPreferenceWins) {
|
||||||
|
ColorEnvironment forcing = {.force_color = "3", .term = "xterm-256color"};
|
||||||
|
EXPECT_EQ(ChooseColorMode(Preference::Never, forcing, /*is_terminal=*/true),
|
||||||
|
ColorMode::NoColor);
|
||||||
|
|
||||||
|
ColorEnvironment disabling = {.no_color = "1", .term = "dumb"};
|
||||||
|
EXPECT_EQ(ChooseColorMode(Preference::Always, disabling,
|
||||||
|
/*is_terminal=*/false),
|
||||||
|
ColorMode::Ansi16);
|
||||||
|
|
||||||
|
// Forcing color on without any hint of what the terminal handles gets the
|
||||||
|
// depth every color terminal supports.
|
||||||
|
EXPECT_EQ(ChooseColorMode(Preference::Always, {}, /*is_terminal=*/false),
|
||||||
|
ColorMode::Ansi16);
|
||||||
|
EXPECT_EQ(ChooseColorMode(Preference::Always, ColorTerminal(),
|
||||||
|
/*is_terminal=*/false),
|
||||||
|
ColorMode::Ansi256);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(CapabilitiesTest, NoColor) {
|
||||||
|
// https://no-color.org: any non-empty value disables color, whatever it is.
|
||||||
|
EXPECT_EQ(OnTerminal({.no_color = "1", .term = "xterm-256color"}),
|
||||||
|
ColorMode::NoColor);
|
||||||
|
EXPECT_EQ(OnTerminal({.no_color = "0", .term = "xterm-256color"}),
|
||||||
|
ColorMode::NoColor);
|
||||||
|
|
||||||
|
// Being set to the empty string carries no meaning, so it must not disable
|
||||||
|
// color: an empty variable inherited from a wrapper script would otherwise
|
||||||
|
// silently turn color off everywhere.
|
||||||
|
EXPECT_EQ(OnTerminal({.no_color = "", .term = "xterm-256color"}),
|
||||||
|
ColorMode::Ansi256);
|
||||||
|
|
||||||
|
// It outranks the forcing variables.
|
||||||
|
EXPECT_EQ(OnTerminal({.no_color = "1", .force_color = "3"}),
|
||||||
|
ColorMode::NoColor);
|
||||||
|
EXPECT_EQ(OnTerminal({.no_color = "1", .clicolor_force = "1"}),
|
||||||
|
ColorMode::NoColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(CapabilitiesTest, ForceColor) {
|
||||||
|
// Color even without a terminal, at the depth the level names.
|
||||||
|
EXPECT_EQ(OffTerminal({.force_color = "1"}), ColorMode::Ansi16);
|
||||||
|
EXPECT_EQ(OffTerminal({.force_color = "2"}), ColorMode::Ansi256);
|
||||||
|
EXPECT_EQ(OffTerminal({.force_color = "3"}), ColorMode::Truecolor);
|
||||||
|
|
||||||
|
// The level overrides what the terminal claims.
|
||||||
|
EXPECT_EQ(OnTerminal({.force_color = "1", .colorterm = "truecolor"}),
|
||||||
|
ColorMode::Ansi16);
|
||||||
|
|
||||||
|
// Any other non-empty value enables color without naming a depth.
|
||||||
|
EXPECT_EQ(OffTerminal({.force_color = "true"}), ColorMode::Ansi16);
|
||||||
|
EXPECT_EQ(OffTerminal({.force_color = "true", .term = "xterm-256color"}),
|
||||||
|
ColorMode::Ansi256);
|
||||||
|
|
||||||
|
// Zero disables color outright, even on a capable terminal.
|
||||||
|
EXPECT_EQ(OnTerminal({.force_color = "0", .term = "xterm-256color"}),
|
||||||
|
ColorMode::NoColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(CapabilitiesTest, EmptyValuesCarryNoOpinion) {
|
||||||
|
// An empty variable means the same as an unset one throughout, so a wrapper
|
||||||
|
// script that exports one without a value changes nothing.
|
||||||
|
EXPECT_EQ(OffTerminal({.force_color = ""}), ColorMode::NoColor);
|
||||||
|
EXPECT_EQ(OffTerminal({.clicolor_force = ""}), ColorMode::NoColor);
|
||||||
|
EXPECT_EQ(OnTerminal({.no_color = "", .term = "xterm-256color"}),
|
||||||
|
ColorMode::Ansi256);
|
||||||
|
EXPECT_EQ(OnTerminal({.clicolor = "", .term = "xterm-256color"}),
|
||||||
|
ColorMode::Ansi256);
|
||||||
|
EXPECT_EQ(OnTerminal({.force_color = "", .term = "xterm-256color"}),
|
||||||
|
ColorMode::Ansi256);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(CapabilitiesTest, CliColor) {
|
||||||
|
// The BSD convention: `CLICOLOR_FORCE` enables color off a terminal, and
|
||||||
|
// `CLICOLOR=0` disables it on one.
|
||||||
|
EXPECT_EQ(OffTerminal({.clicolor_force = "1"}), ColorMode::Ansi16);
|
||||||
|
EXPECT_EQ(OffTerminal({.clicolor_force = "1", .term = "xterm-256color"}),
|
||||||
|
ColorMode::Ansi256);
|
||||||
|
// `0` means "don't force", not "disable", so a terminal still gets color.
|
||||||
|
EXPECT_EQ(OnTerminal({.clicolor_force = "0", .term = "xterm-256color"}),
|
||||||
|
ColorMode::Ansi256);
|
||||||
|
EXPECT_EQ(OffTerminal({.clicolor_force = "0"}), ColorMode::NoColor);
|
||||||
|
|
||||||
|
EXPECT_EQ(OnTerminal({.clicolor = "0", .term = "xterm-256color"}),
|
||||||
|
ColorMode::NoColor);
|
||||||
|
EXPECT_EQ(OnTerminal({.clicolor = "1", .term = "xterm-256color"}),
|
||||||
|
ColorMode::Ansi256);
|
||||||
|
|
||||||
|
// Forcing beats disabling.
|
||||||
|
EXPECT_EQ(OnTerminal({.clicolor_force = "1", .clicolor = "0"}),
|
||||||
|
ColorMode::Ansi16);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(CapabilitiesTest, ColorDepthFromColorterm) {
|
||||||
|
EXPECT_EQ(OnTerminal({.colorterm = "truecolor", .term = "xterm"}),
|
||||||
|
ColorMode::Truecolor);
|
||||||
|
EXPECT_EQ(OnTerminal({.colorterm = "24bit", .term = "xterm"}),
|
||||||
|
ColorMode::Truecolor);
|
||||||
|
EXPECT_EQ(OnTerminal({.colorterm = "", .term = "xterm"}), ColorMode::Ansi16);
|
||||||
|
|
||||||
|
// `COLORTERM` can't enable color off a terminal, so a rich value inherited
|
||||||
|
// by a redirected stream can't smuggle escapes into it.
|
||||||
|
EXPECT_EQ(OffTerminal({.colorterm = "truecolor", .term = "xterm"}),
|
||||||
|
ColorMode::NoColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(CapabilitiesTest, ColorDepthFromTermProgram) {
|
||||||
|
EXPECT_EQ(OnTerminal({.term_program = "vscode", .term = "xterm"}),
|
||||||
|
ColorMode::Truecolor);
|
||||||
|
EXPECT_EQ(OnTerminal({.term_program = "iTerm.app", .term = "xterm"}),
|
||||||
|
ColorMode::Truecolor);
|
||||||
|
EXPECT_EQ(OnTerminal({.term_program = "WarpTerminal", .term = "xterm"}),
|
||||||
|
ColorMode::Truecolor);
|
||||||
|
EXPECT_EQ(OnTerminal({.term_program = "Hyper", .term = "xterm"}),
|
||||||
|
ColorMode::Truecolor);
|
||||||
|
EXPECT_EQ(OnTerminal({.term_program = "Tabby", .term = "xterm"}),
|
||||||
|
ColorMode::Truecolor);
|
||||||
|
EXPECT_EQ(OnTerminal({.term_program = "Terminus", .term = "xterm"}),
|
||||||
|
ColorMode::Truecolor);
|
||||||
|
// Apple's Terminal renders only the 256-color palette.
|
||||||
|
EXPECT_EQ(OnTerminal({.term_program = "Apple_Terminal", .term = "xterm"}),
|
||||||
|
ColorMode::Ansi256);
|
||||||
|
EXPECT_EQ(OnTerminal({.term_program = "unknown", .term = "xterm-256color"}),
|
||||||
|
ColorMode::Ansi256);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(CapabilitiesTest, ColorDepthFromTerm) {
|
||||||
|
EXPECT_EQ(OnTerminal({.term = "xterm-kitty"}), ColorMode::Truecolor);
|
||||||
|
EXPECT_EQ(OnTerminal({.term = "alacritty"}), ColorMode::Truecolor);
|
||||||
|
EXPECT_EQ(OnTerminal({.term = "wezterm"}), ColorMode::Truecolor);
|
||||||
|
EXPECT_EQ(OnTerminal({.term = "foot-extra"}), ColorMode::Truecolor);
|
||||||
|
EXPECT_EQ(OnTerminal({.term = "xterm-direct"}), ColorMode::Truecolor);
|
||||||
|
EXPECT_EQ(OnTerminal({.term = "ghostty"}), ColorMode::Truecolor);
|
||||||
|
EXPECT_EQ(OnTerminal({.term = "contour-latest"}), ColorMode::Truecolor);
|
||||||
|
EXPECT_EQ(OnTerminal({.term = "xterm-truecolor"}), ColorMode::Truecolor);
|
||||||
|
|
||||||
|
EXPECT_EQ(OnTerminal({.term = "xterm-256color"}), ColorMode::Ansi256);
|
||||||
|
EXPECT_EQ(OnTerminal({.term = "screen-256color"}), ColorMode::Ansi256);
|
||||||
|
EXPECT_EQ(OnTerminal({.term = "putty-256"}), ColorMode::Ansi256);
|
||||||
|
|
||||||
|
// A terminal matching both a truecolor and a 256-color pattern takes the
|
||||||
|
// richer one, so the order these are tried in is load-bearing.
|
||||||
|
EXPECT_EQ(OnTerminal({.term = "vte-256color"}), ColorMode::Truecolor);
|
||||||
|
|
||||||
|
// Known to render color, but with nothing saying how much.
|
||||||
|
EXPECT_EQ(OnTerminal({.term = "xterm"}), ColorMode::Ansi16);
|
||||||
|
EXPECT_EQ(OnTerminal({.term = "linux"}), ColorMode::Ansi16);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(CapabilitiesTest, Charset) {
|
||||||
|
EXPECT_EQ(ChooseCharset(Preference::Auto, "en_US.UTF-8"), Charset::Utf8);
|
||||||
|
EXPECT_EQ(ChooseCharset(Preference::Auto, "C.utf8"), Charset::Utf8);
|
||||||
|
EXPECT_EQ(ChooseCharset(Preference::Auto, "en_US.utf-8"), Charset::Utf8);
|
||||||
|
|
||||||
|
// Drawing box characters into a terminal decoding something else turns them
|
||||||
|
// into several bytes of mojibake and destroys the alignment they were for.
|
||||||
|
EXPECT_EQ(ChooseCharset(Preference::Auto, "C"), Charset::Ascii);
|
||||||
|
EXPECT_EQ(ChooseCharset(Preference::Auto, "POSIX"), Charset::Ascii);
|
||||||
|
EXPECT_EQ(ChooseCharset(Preference::Auto, "en_US.ISO-8859-1"),
|
||||||
|
Charset::Ascii);
|
||||||
|
EXPECT_EQ(ChooseCharset(Preference::Auto, ""), Charset::Ascii);
|
||||||
|
|
||||||
|
EXPECT_EQ(ChooseCharset(Preference::Never, "en_US.UTF-8"), Charset::Ascii);
|
||||||
|
EXPECT_EQ(ChooseCharset(Preference::Always, "C"), Charset::Utf8);
|
||||||
|
}
|
||||||
|
|
||||||
|
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_FALSE(capabilities.is_terminal);
|
||||||
|
EXPECT_FALSE(capabilities.columns.has_value());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(CapabilitiesTest, Detect) {
|
||||||
|
// Detection reads the process environment and the descriptor it is handed, so
|
||||||
|
// only what neither can change is pinned here. What the policy decides from
|
||||||
|
// given inputs is tested above, against `ChooseColorMode` and `ChooseCharset`
|
||||||
|
// directly.
|
||||||
|
//
|
||||||
|
// It detects against a file rather than the process's own streams: those are
|
||||||
|
// a pipe under the test runner but a terminal under a debugger, and an
|
||||||
|
// exported `FORCE_COLOR` turns color on for either.
|
||||||
|
auto dir = Filesystem::MakeTmpDir();
|
||||||
|
ASSERT_TRUE(dir.ok()) << dir.error();
|
||||||
|
auto file = dir->OpenWriteOnly("out", Filesystem::CreationOptions::CreateNew);
|
||||||
|
ASSERT_TRUE(file.ok()) << file.error();
|
||||||
|
|
||||||
|
Capabilities capabilities = Capabilities::Detect(*file);
|
||||||
|
// A file is never a terminal.
|
||||||
|
EXPECT_FALSE(capabilities.is_terminal);
|
||||||
|
// `COLUMNS` reaches detection from the environment, so whether a width is
|
||||||
|
// found depends on it, but one that is found is usable.
|
||||||
|
if (capabilities.columns) {
|
||||||
|
EXPECT_GT(*capabilities.columns, 0);
|
||||||
|
}
|
||||||
|
EXPECT_GT(capabilities.tab_width, 0);
|
||||||
|
|
||||||
|
// A preference decides on its own, whatever the environment holds. Color
|
||||||
|
// forced on picks a depth from the environment, so only that it is on can be
|
||||||
|
// pinned here.
|
||||||
|
EXPECT_EQ(Capabilities::Detect(
|
||||||
|
*file, {.color = Preference::Never, .utf8 = Preference::Never})
|
||||||
|
.color_mode,
|
||||||
|
ColorMode::NoColor);
|
||||||
|
EXPECT_NE(
|
||||||
|
Capabilities::Detect(*file, {.color = Preference::Always}).color_mode,
|
||||||
|
ColorMode::NoColor);
|
||||||
|
EXPECT_EQ(Capabilities::Detect(*file, {.utf8 = Preference::Always}).charset,
|
||||||
|
Charset::Utf8);
|
||||||
|
EXPECT_EQ(Capabilities::Detect(*file, {.utf8 = Preference::Never}).charset,
|
||||||
|
Charset::Ascii);
|
||||||
|
|
||||||
|
(*std::move(file)).Close().Check();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
} // namespace Carbon::Terminal
|
||||||
@@ -0,0 +1,239 @@
|
|||||||
|
// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
|
||||||
|
// Exceptions. See /LICENSE for license information.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
||||||
|
|
||||||
|
#include "common/terminal/color.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
|
||||||
|
#include "common/check.h"
|
||||||
|
#include "llvm/ADT/StringRef.h"
|
||||||
|
#include "llvm/Support/Format.h"
|
||||||
|
|
||||||
|
namespace Carbon::Terminal {
|
||||||
|
|
||||||
|
static constexpr int AnsiColorCount = 16;
|
||||||
|
|
||||||
|
// Reference values for the 16 ANSI colors.
|
||||||
|
//
|
||||||
|
// Nothing standardizes these: a terminal draws them from the user's palette,
|
||||||
|
// which is exactly what makes them worth using. But downsampling an RGB color
|
||||||
|
// still needs some notion of where each named color sits, so these use the
|
||||||
|
// xterm defaults, which terminals vary from but stay recognizably near.
|
||||||
|
static constexpr std::array<Color::RgbValue, AnsiColorCount> AnsiColorRgbs = {{
|
||||||
|
{.r = 0, .g = 0, .b = 0}, // Black
|
||||||
|
{.r = 205, .g = 0, .b = 0}, // Red
|
||||||
|
{.r = 0, .g = 205, .b = 0}, // Green
|
||||||
|
{.r = 205, .g = 205, .b = 0}, // Yellow
|
||||||
|
{.r = 0, .g = 0, .b = 238}, // Blue
|
||||||
|
{.r = 205, .g = 0, .b = 205}, // Magenta
|
||||||
|
{.r = 0, .g = 205, .b = 205}, // Cyan
|
||||||
|
{.r = 229, .g = 229, .b = 229}, // White
|
||||||
|
{.r = 127, .g = 127, .b = 127}, // BrightBlack
|
||||||
|
{.r = 255, .g = 0, .b = 0}, // BrightRed
|
||||||
|
{.r = 0, .g = 255, .b = 0}, // BrightGreen
|
||||||
|
{.r = 255, .g = 255, .b = 0}, // BrightYellow
|
||||||
|
{.r = 92, .g = 92, .b = 255}, // BrightBlue
|
||||||
|
{.r = 255, .g = 0, .b = 255}, // BrightMagenta
|
||||||
|
{.r = 0, .g = 255, .b = 255}, // BrightCyan
|
||||||
|
{.r = 255, .g = 255, .b = 255}, // BrightWhite
|
||||||
|
}};
|
||||||
|
|
||||||
|
static constexpr std::array<llvm::StringRef, AnsiColorCount> AnsiColorNames = {
|
||||||
|
"Black", "Red", "Green", "Yellow",
|
||||||
|
"Blue", "Magenta", "Cyan", "White",
|
||||||
|
"BrightBlack", "BrightRed", "BrightGreen", "BrightYellow",
|
||||||
|
"BrightBlue", "BrightMagenta", "BrightCyan", "BrightWhite"};
|
||||||
|
|
||||||
|
// Returns the "redmean" distance between two colors, squared and scaled by 256
|
||||||
|
// to keep it in integer arithmetic.
|
||||||
|
//
|
||||||
|
// Treating the channels as orthogonal axes is cheaper but sits a long way from
|
||||||
|
// perceived difference, and downsampling is exactly where that shows: a color
|
||||||
|
// picked for a diagnostic lands on whichever of a small fixed set the
|
||||||
|
// arithmetic says is closest, and a plain Euclidean fit underweights green,
|
||||||
|
// where the eye is most sensitive. Redmean weights the
|
||||||
|
// channels by where the pair sits on the red axis, which tracks perception far
|
||||||
|
// better for a couple of extra multiplies:
|
||||||
|
// https://en.wikipedia.org/wiki/Color_difference#sRGB
|
||||||
|
//
|
||||||
|
// The formula ends in a square root, which is dropped because only the ordering
|
||||||
|
// is used. Scaling by 256 turns the two fractional weights into integers; the
|
||||||
|
// result peaks just under 150 million, well inside the range.
|
||||||
|
static auto DistanceSquared(Color::RgbValue lhs, Color::RgbValue rhs) -> int {
|
||||||
|
int red_mean = (static_cast<int>(lhs.r) + static_cast<int>(rhs.r)) / 2;
|
||||||
|
int dr = static_cast<int>(lhs.r) - static_cast<int>(rhs.r);
|
||||||
|
int dg = static_cast<int>(lhs.g) - static_cast<int>(rhs.g);
|
||||||
|
int db = static_cast<int>(lhs.b) - static_cast<int>(rhs.b);
|
||||||
|
return (512 + red_mean) * dr * dr + 1024 * dg * dg +
|
||||||
|
(767 - red_mean) * db * db;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the ANSI color whose reference value is nearest to `rgb`.
|
||||||
|
static auto NearestAnsiColor(Color::RgbValue rgb) -> AnsiColor {
|
||||||
|
int best_index = 0;
|
||||||
|
int best_distance = DistanceSquared(rgb, AnsiColorRgbs[0]);
|
||||||
|
for (int i = 1; i < AnsiColorCount; ++i) {
|
||||||
|
int distance = DistanceSquared(rgb, AnsiColorRgbs[i]);
|
||||||
|
if (distance < best_distance) {
|
||||||
|
best_distance = distance;
|
||||||
|
best_index = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return static_cast<AnsiColor>(best_index);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The channel values of the 6x6x6 color cube at palette indices 16 through
|
||||||
|
// 231. The first step is much larger than the rest, so a channel can't be
|
||||||
|
// rounded to the nearest level by dividing.
|
||||||
|
static constexpr std::array<uint8_t, 6> CubeLevels = {0, 95, 135,
|
||||||
|
175, 215, 255};
|
||||||
|
|
||||||
|
// The midpoints between adjacent entries of `CubeLevels`, which are where the
|
||||||
|
// nearest level changes.
|
||||||
|
static constexpr std::array<uint8_t, 5> CubeLevelMidpoints = {48, 115, 155, 195,
|
||||||
|
235};
|
||||||
|
|
||||||
|
static_assert(
|
||||||
|
[] {
|
||||||
|
for (size_t i = 0; i < CubeLevelMidpoints.size(); ++i) {
|
||||||
|
// Rounded up, so that a value exactly between two levels takes the
|
||||||
|
// higher one.
|
||||||
|
if (CubeLevelMidpoints[i] !=
|
||||||
|
(CubeLevels[i] + CubeLevels[i + 1] + 1) / 2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}(),
|
||||||
|
"Midpoints must stay in step with the levels they separate.");
|
||||||
|
|
||||||
|
// Returns the index into `CubeLevels` of the level nearest `value`.
|
||||||
|
static auto NearestCubeLevel(uint8_t value) -> int {
|
||||||
|
int level = 0;
|
||||||
|
while (level < static_cast<int>(CubeLevelMidpoints.size()) &&
|
||||||
|
value >= CubeLevelMidpoints[level]) {
|
||||||
|
++level;
|
||||||
|
}
|
||||||
|
return level;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the 256-color palette index whose color is nearest to `rgb`.
|
||||||
|
static auto NearestPaletteIndex(Color::RgbValue rgb) -> uint8_t {
|
||||||
|
// Only the color cube and the gray ramp are considered. Indices 0 through 15
|
||||||
|
// alias the ANSI colors, whose appearance comes from the user's palette, so
|
||||||
|
// an exact RGB request must never be answered with one.
|
||||||
|
int r_level = NearestCubeLevel(rgb.r);
|
||||||
|
int g_level = NearestCubeLevel(rgb.g);
|
||||||
|
int b_level = NearestCubeLevel(rgb.b);
|
||||||
|
Color::RgbValue cube = {.r = CubeLevels[r_level],
|
||||||
|
.g = CubeLevels[g_level],
|
||||||
|
.b = CubeLevels[b_level]};
|
||||||
|
|
||||||
|
// The gray ramp at indices 232 through 255 runs from 8 to 238 in steps of
|
||||||
|
// 10, and is finer than the cube's gray diagonal for near-neutral colors.
|
||||||
|
int average = (static_cast<int>(rgb.r) + static_cast<int>(rgb.g) +
|
||||||
|
static_cast<int>(rgb.b)) /
|
||||||
|
3;
|
||||||
|
int gray_step = std::clamp((average - 8 + 5) / 10, 0, 23);
|
||||||
|
auto gray_value = static_cast<uint8_t>(8 + 10 * gray_step);
|
||||||
|
Color::RgbValue gray = {.r = gray_value, .g = gray_value, .b = gray_value};
|
||||||
|
|
||||||
|
if (DistanceSquared(rgb, gray) < DistanceSquared(rgb, cube)) {
|
||||||
|
return 232 + gray_step;
|
||||||
|
}
|
||||||
|
return 16 + 36 * r_level + 6 * g_level + b_level;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the SGR parameter selecting `color` for `target`.
|
||||||
|
//
|
||||||
|
// The original ANSI codes cover the first eight colors, and the later "bright"
|
||||||
|
// codes cover the rest at a fixed offset.
|
||||||
|
static auto AnsiSgrCode(AnsiColor color, ColorTarget target) -> uint8_t {
|
||||||
|
CARBON_CHECK(target != ColorTarget::Underline,
|
||||||
|
"Underline color has no direct ANSI form.");
|
||||||
|
int index = static_cast<int>(color);
|
||||||
|
int base = target == ColorTarget::Background ? 40 : 30;
|
||||||
|
if (index >= 8) {
|
||||||
|
// Bright foregrounds are 90-97 and bright backgrounds 100-107.
|
||||||
|
base += 60;
|
||||||
|
}
|
||||||
|
return base + (index % 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the SGR parameter introducing an extended color for `target`, which
|
||||||
|
// is followed by either `;5;<index>` or `;2;<r>;<g>;<b>`.
|
||||||
|
static auto ExtendedSgrCode(ColorTarget target) -> uint8_t {
|
||||||
|
switch (target) {
|
||||||
|
case ColorTarget::Foreground:
|
||||||
|
return 38;
|
||||||
|
case ColorTarget::Background:
|
||||||
|
return 48;
|
||||||
|
case ColorTarget::Underline:
|
||||||
|
return 58;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Color::AppendEscape(OutputBufferRef out, ColorMode mode,
|
||||||
|
ColorTarget target) const -> void {
|
||||||
|
if (mode == ColorMode::NoColor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Underline colors are only expressible through the extended-color escape,
|
||||||
|
// which `Ansi16` doesn't use.
|
||||||
|
if (target == ColorTarget::Underline && mode == ColorMode::Ansi16) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CARBON_CHECK(is_set(), "Only a color that is set can be selected.");
|
||||||
|
|
||||||
|
if (kind_ == Kind::Ansi) {
|
||||||
|
if (target == ColorTarget::Underline) {
|
||||||
|
// Named underline colors go through the palette form of the extended
|
||||||
|
// escape, as there is no direct code for them.
|
||||||
|
out.Append("\x1b[58;5;", static_cast<uint8_t>(ansi()), "m");
|
||||||
|
} else {
|
||||||
|
out.Append("\x1b[", AnsiSgrCode(ansi(), target), "m");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (mode) {
|
||||||
|
case ColorMode::Truecolor:
|
||||||
|
out.Append("\x1b[", ExtendedSgrCode(target), ";2;", channels_.r, ";",
|
||||||
|
channels_.g, ";", channels_.b, "m");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ColorMode::Ansi256:
|
||||||
|
out.Append("\x1b[", ExtendedSgrCode(target), ";5;",
|
||||||
|
NearestPaletteIndex(channels_), "m");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ColorMode::Ansi16:
|
||||||
|
out.Append("\x1b[", AnsiSgrCode(NearestAnsiColor(channels_), target),
|
||||||
|
"m");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ColorMode::NoColor:
|
||||||
|
CARBON_FATAL("Returned above without emitting anything.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Color::Print(llvm::raw_ostream& out) const -> void {
|
||||||
|
switch (kind_) {
|
||||||
|
case Kind::None:
|
||||||
|
out << "None";
|
||||||
|
return;
|
||||||
|
case Kind::Ansi:
|
||||||
|
out << AnsiColorNames[static_cast<int>(ansi())];
|
||||||
|
return;
|
||||||
|
case Kind::Rgb:
|
||||||
|
out << llvm::format("#%02x%02x%02x", channels_.r, channels_.g,
|
||||||
|
channels_.b);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Carbon::Terminal
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
|
||||||
|
// Exceptions. See /LICENSE for license information.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
||||||
|
|
||||||
|
#ifndef CARBON_COMMON_TERMINAL_COLOR_H_
|
||||||
|
#define CARBON_COMMON_TERMINAL_COLOR_H_
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
#include "common/check.h"
|
||||||
|
#include "common/ostream.h"
|
||||||
|
#include "common/terminal/output_buffer_ref.h"
|
||||||
|
|
||||||
|
namespace Carbon::Terminal {
|
||||||
|
|
||||||
|
// The color escape sequences a terminal understands.
|
||||||
|
//
|
||||||
|
// Colors, like the other text attributes, are selected with Select Graphic
|
||||||
|
// Rendition (SGR) escape sequences. The sequence and the codes for the first
|
||||||
|
// eight colors come from ECMA-48, published in parallel as ANSI X3.64;
|
||||||
|
// terminal emulators added the bright variants, the 256-color palette, and the
|
||||||
|
// 24-bit form:
|
||||||
|
// https://ecma-international.org/publications-and-standards/standards/ecma-48/
|
||||||
|
//
|
||||||
|
// These form a ladder: each mode can express everything the modes before it
|
||||||
|
// can. Colors that the active mode can't express exactly are downsampled to
|
||||||
|
// the nearest color it can, so callers author in the richest form and let
|
||||||
|
// rendering degrade on its own.
|
||||||
|
enum class ColorMode : int8_t {
|
||||||
|
// Emit no escape sequences at all, producing plain text.
|
||||||
|
NoColor,
|
||||||
|
// The 16 colors with SGR codes of their own.
|
||||||
|
Ansi16,
|
||||||
|
// The 256-color palette: the 16 ANSI colors, a 6x6x6 RGB cube, and a 24-step
|
||||||
|
// gray ramp.
|
||||||
|
Ansi256,
|
||||||
|
// Direct 24-bit RGB, commonly called "truecolor".
|
||||||
|
Truecolor,
|
||||||
|
};
|
||||||
|
|
||||||
|
// The 16 colors with SGR codes of their own.
|
||||||
|
//
|
||||||
|
// Terminals render these through the user's configured palette, which makes
|
||||||
|
// them the right choice for output that should blend with the user's theme.
|
||||||
|
// The tradeoff is that their rendered appearance is outside our control: a
|
||||||
|
// user's "red" may be any color at all.
|
||||||
|
enum class AnsiColor : uint8_t {
|
||||||
|
Black,
|
||||||
|
Red,
|
||||||
|
Green,
|
||||||
|
Yellow,
|
||||||
|
Blue,
|
||||||
|
Magenta,
|
||||||
|
Cyan,
|
||||||
|
White,
|
||||||
|
BrightBlack,
|
||||||
|
BrightRed,
|
||||||
|
BrightGreen,
|
||||||
|
BrightYellow,
|
||||||
|
BrightBlue,
|
||||||
|
BrightMagenta,
|
||||||
|
BrightCyan,
|
||||||
|
BrightWhite,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Which part of a cell's rendering a color applies to.
|
||||||
|
enum class ColorTarget : int8_t {
|
||||||
|
Foreground,
|
||||||
|
Background,
|
||||||
|
// The color of the underline itself, independent of the foreground. Only
|
||||||
|
// `Ansi256` and richer modes can express this.
|
||||||
|
Underline,
|
||||||
|
};
|
||||||
|
|
||||||
|
// A color to render with: one of the 16 named ANSI colors, a 24-bit RGB value,
|
||||||
|
// or no color at all.
|
||||||
|
//
|
||||||
|
// RGB colors render exactly where the terminal supports them, and are
|
||||||
|
// downsampled where it doesn't. Downsampling to `Ansi16` measures distance
|
||||||
|
// against fixed reference values, but the terminal renders the result from the
|
||||||
|
// user's palette, so a downsampled color can land far from the original.
|
||||||
|
// Prefer `AnsiColor` wherever output should track the user's theme, and RGB
|
||||||
|
// only where an exact color matters.
|
||||||
|
//
|
||||||
|
// A default-constructed color selects nothing. That is how a `Style` spells
|
||||||
|
// leaving one of its colors to the terminal, so this is a value with an empty
|
||||||
|
// state rather than something wrapped in an `optional` to get one.
|
||||||
|
class Color : public Printable<Color> {
|
||||||
|
public:
|
||||||
|
// Whether a color names a palette entry, gives channel values directly, or
|
||||||
|
// selects nothing.
|
||||||
|
enum class Kind : uint8_t {
|
||||||
|
None,
|
||||||
|
Ansi,
|
||||||
|
Rgb,
|
||||||
|
};
|
||||||
|
|
||||||
|
// The channel values of a 24-bit color.
|
||||||
|
struct RgbValue {
|
||||||
|
uint8_t r;
|
||||||
|
uint8_t g;
|
||||||
|
uint8_t b;
|
||||||
|
|
||||||
|
friend auto operator==(RgbValue lhs, RgbValue rhs) -> bool = default;
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr Color() = default;
|
||||||
|
|
||||||
|
// Colors convert implicitly from `AnsiColor` so that call sites can read as
|
||||||
|
// `style.Foreground(AnsiColor::Red)`.
|
||||||
|
//
|
||||||
|
// NOLINTNEXTLINE(google-explicit-constructor)
|
||||||
|
constexpr Color(AnsiColor ansi)
|
||||||
|
: kind_(Kind::Ansi), channels_{.r = static_cast<uint8_t>(ansi)} {}
|
||||||
|
|
||||||
|
constexpr Color(uint8_t r, uint8_t g, uint8_t b)
|
||||||
|
: kind_(Kind::Rgb), channels_{.r = r, .g = g, .b = b} {}
|
||||||
|
|
||||||
|
auto kind() const -> Kind { return kind_; }
|
||||||
|
|
||||||
|
// Returns whether this selects a color at all.
|
||||||
|
auto is_set() const -> bool { return kind_ != Kind::None; }
|
||||||
|
|
||||||
|
// Returns the named color. Valid only when `kind()` is `Ansi`.
|
||||||
|
auto ansi() const -> AnsiColor {
|
||||||
|
CARBON_CHECK(kind_ == Kind::Ansi,
|
||||||
|
"Only a named color has a palette index.");
|
||||||
|
return static_cast<AnsiColor>(channels_.r);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the channel values. Valid only when `kind()` is `Rgb`.
|
||||||
|
auto rgb() const -> RgbValue {
|
||||||
|
CARBON_CHECK(kind_ == Kind::Rgb, "Only an RGB color has channel values.");
|
||||||
|
return channels_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Appends the escape sequence selecting this color for `target`, which
|
||||||
|
// requires that one is set.
|
||||||
|
//
|
||||||
|
// Appends nothing when `mode` is `NoColor`, or when `target` is `Underline`
|
||||||
|
// and `mode` is `Ansi16`, which has no way to express an underline color.
|
||||||
|
auto AppendEscape(OutputBufferRef out, ColorMode mode,
|
||||||
|
ColorTarget target) const -> void;
|
||||||
|
|
||||||
|
auto Print(llvm::raw_ostream& out) const -> void;
|
||||||
|
|
||||||
|
// Written out rather than defaulted because the `Printable` base has no
|
||||||
|
// comparison of its own, which would leave a defaulted one deleted.
|
||||||
|
friend auto operator==(Color lhs, Color rhs) -> bool {
|
||||||
|
return lhs.kind_ == rhs.kind_ && lhs.channels_ == rhs.channels_;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
Kind kind_ = Kind::None;
|
||||||
|
|
||||||
|
// The palette index in `r` with the rest zero for `Ansi`, the channel values
|
||||||
|
// for `Rgb`, and all zero for `None`.
|
||||||
|
//
|
||||||
|
// Overlapping the two in a union would leave the bytes past a palette index
|
||||||
|
// unwritten. Every byte carrying part of the value is what lets a whole
|
||||||
|
// `Style` be compared as bytes, and an index fits in a channel anyway.
|
||||||
|
RgbValue channels_ = {.r = 0, .g = 0, .b = 0};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Carbon::Terminal
|
||||||
|
|
||||||
|
#endif // CARBON_COMMON_TERMINAL_COLOR_H_
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
|
||||||
|
// Exceptions. See /LICENSE for license information.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
||||||
|
|
||||||
|
#include "common/terminal/color.h"
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include "llvm/ADT/SmallString.h"
|
||||||
|
#include "llvm/ADT/StringExtras.h"
|
||||||
|
|
||||||
|
namespace Carbon::Terminal {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
auto Escape(Color color, ColorMode mode,
|
||||||
|
ColorTarget target = ColorTarget::Foreground) -> std::string {
|
||||||
|
llvm::SmallString<32> escape;
|
||||||
|
color.AppendEscape(escape, mode, target);
|
||||||
|
return std::string(escape);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ColorTest, AnsiEscapes) {
|
||||||
|
// Named colors use their own SGR codes rather than the extended forms, in
|
||||||
|
// every mode that has color at all, so the terminal renders them from the
|
||||||
|
// user's palette.
|
||||||
|
for (ColorMode mode :
|
||||||
|
{ColorMode::Ansi16, ColorMode::Ansi256, ColorMode::Truecolor}) {
|
||||||
|
EXPECT_EQ(Escape(AnsiColor::Red, mode), "\x1b[31m");
|
||||||
|
EXPECT_EQ(Escape(AnsiColor::Black, mode), "\x1b[30m");
|
||||||
|
EXPECT_EQ(Escape(AnsiColor::BrightCyan, mode), "\x1b[96m");
|
||||||
|
EXPECT_EQ(Escape(AnsiColor::Red, mode, ColorTarget::Background),
|
||||||
|
"\x1b[41m");
|
||||||
|
EXPECT_EQ(Escape(AnsiColor::BrightWhite, mode, ColorTarget::Background),
|
||||||
|
"\x1b[107m");
|
||||||
|
}
|
||||||
|
|
||||||
|
EXPECT_EQ(Escape(AnsiColor::Red, ColorMode::NoColor), "");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ColorTest, RgbEscapes) {
|
||||||
|
Color red(255, 0, 0);
|
||||||
|
EXPECT_EQ(Escape(red, ColorMode::Truecolor), "\x1b[38;2;255;0;0m");
|
||||||
|
EXPECT_EQ(Escape(red, ColorMode::Truecolor, ColorTarget::Background),
|
||||||
|
"\x1b[48;2;255;0;0m");
|
||||||
|
EXPECT_EQ(Escape(red, ColorMode::Ansi256), "\x1b[38;5;196m");
|
||||||
|
EXPECT_EQ(Escape(red, ColorMode::Ansi16), "\x1b[91m");
|
||||||
|
EXPECT_EQ(Escape(red, ColorMode::NoColor), "");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ColorTest, UnderlineEscapes) {
|
||||||
|
// Underline colors only exist in the extended-color escapes, so in `Ansi16`
|
||||||
|
// the terminal draws the underline in the foreground color.
|
||||||
|
EXPECT_EQ(
|
||||||
|
Escape(AnsiColor::Red, ColorMode::Truecolor, ColorTarget::Underline),
|
||||||
|
"\x1b[58;5;1m");
|
||||||
|
EXPECT_EQ(Escape(AnsiColor::Red, ColorMode::Ansi256, ColorTarget::Underline),
|
||||||
|
"\x1b[58;5;1m");
|
||||||
|
EXPECT_EQ(Escape(AnsiColor::Red, ColorMode::Ansi16, ColorTarget::Underline),
|
||||||
|
"");
|
||||||
|
|
||||||
|
Color green(0, 255, 0);
|
||||||
|
EXPECT_EQ(Escape(green, ColorMode::Truecolor, ColorTarget::Underline),
|
||||||
|
"\x1b[58;2;0;255;0m");
|
||||||
|
EXPECT_EQ(Escape(green, ColorMode::Ansi256, ColorTarget::Underline),
|
||||||
|
"\x1b[58;5;46m");
|
||||||
|
EXPECT_EQ(Escape(green, ColorMode::Ansi16, ColorTarget::Underline), "");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ColorTest, DownsampleToAnsi16) {
|
||||||
|
// The reference value of each ANSI color must come back as that color, or
|
||||||
|
// downsampling would shift colors that were already expressible. Spelling
|
||||||
|
// the values out here rather than reading them back from the same table the
|
||||||
|
// implementation uses is what makes this catch a wrong table.
|
||||||
|
struct Expected {
|
||||||
|
Color color;
|
||||||
|
llvm::StringRef escape;
|
||||||
|
};
|
||||||
|
Expected cases[] = {
|
||||||
|
{Color(0, 0, 0), "\x1b[30m"}, {Color(205, 0, 0), "\x1b[31m"},
|
||||||
|
{Color(0, 205, 0), "\x1b[32m"}, {Color(205, 205, 0), "\x1b[33m"},
|
||||||
|
{Color(0, 0, 238), "\x1b[34m"}, {Color(205, 0, 205), "\x1b[35m"},
|
||||||
|
{Color(0, 205, 205), "\x1b[36m"}, {Color(229, 229, 229), "\x1b[37m"},
|
||||||
|
{Color(127, 127, 127), "\x1b[90m"}, {Color(255, 0, 0), "\x1b[91m"},
|
||||||
|
{Color(0, 255, 0), "\x1b[92m"}, {Color(255, 255, 0), "\x1b[93m"},
|
||||||
|
{Color(92, 92, 255), "\x1b[94m"}, {Color(255, 0, 255), "\x1b[95m"},
|
||||||
|
{Color(0, 255, 255), "\x1b[96m"}, {Color(255, 255, 255), "\x1b[97m"},
|
||||||
|
};
|
||||||
|
for (const Expected& expected : cases) {
|
||||||
|
EXPECT_EQ(Escape(expected.color, ColorMode::Ansi16), expected.escape)
|
||||||
|
<< expected.color;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Colors between the reference values land on the nearest one.
|
||||||
|
EXPECT_EQ(Escape(Color(250, 10, 10), ColorMode::Ansi16), "\x1b[91m");
|
||||||
|
EXPECT_EQ(Escape(Color(10, 10, 10), ColorMode::Ansi16), "\x1b[30m");
|
||||||
|
EXPECT_EQ(Escape(Color(120, 120, 120), ColorMode::Ansi16), "\x1b[90m");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ColorTest, DownsampleToPalette) {
|
||||||
|
// The corners of the 6x6x6 cube are exactly representable.
|
||||||
|
EXPECT_EQ(Escape(Color(0, 0, 0), ColorMode::Ansi256), "\x1b[38;5;16m");
|
||||||
|
EXPECT_EQ(Escape(Color(255, 255, 255), ColorMode::Ansi256), "\x1b[38;5;231m");
|
||||||
|
EXPECT_EQ(Escape(Color(255, 0, 0), ColorMode::Ansi256), "\x1b[38;5;196m");
|
||||||
|
EXPECT_EQ(Escape(Color(0, 0, 255), ColorMode::Ansi256), "\x1b[38;5;21m");
|
||||||
|
|
||||||
|
// The cube's levels are unevenly spaced, so rounding has to account for that
|
||||||
|
// rather than divide: 95 and 135 are adjacent levels only 40 apart.
|
||||||
|
EXPECT_EQ(Escape(Color(95, 0, 0), ColorMode::Ansi256), "\x1b[38;5;52m");
|
||||||
|
EXPECT_EQ(Escape(Color(130, 0, 0), ColorMode::Ansi256), "\x1b[38;5;88m");
|
||||||
|
|
||||||
|
// Near-neutral colors land on the gray ramp, which is far finer than the
|
||||||
|
// cube's diagonal, except at the ends where the cube wins.
|
||||||
|
EXPECT_EQ(Escape(Color(8, 8, 8), ColorMode::Ansi256), "\x1b[38;5;232m");
|
||||||
|
EXPECT_EQ(Escape(Color(128, 128, 128), ColorMode::Ansi256), "\x1b[38;5;244m");
|
||||||
|
EXPECT_EQ(Escape(Color(238, 238, 238), ColorMode::Ansi256), "\x1b[38;5;255m");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ColorTest, DownsampleAvoidsPaletteEntries) {
|
||||||
|
// Indices 0 through 15 render from the user's palette, so an exact RGB
|
||||||
|
// request must never be answered with one.
|
||||||
|
for (int r = 0; r < 256; r += 17) {
|
||||||
|
for (int g = 0; g < 256; g += 17) {
|
||||||
|
for (int b = 0; b < 256; b += 17) {
|
||||||
|
Color color(r, g, b);
|
||||||
|
std::string escape = Escape(color, ColorMode::Ansi256);
|
||||||
|
int index = 0;
|
||||||
|
ASSERT_TRUE(llvm::to_integer(
|
||||||
|
llvm::StringRef(escape).drop_front(7).drop_back(1), index))
|
||||||
|
<< color;
|
||||||
|
EXPECT_GE(index, 16) << color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ColorTest, Equality) {
|
||||||
|
EXPECT_EQ(Color(AnsiColor::Red), Color(AnsiColor::Red));
|
||||||
|
EXPECT_NE(Color(AnsiColor::Red), Color(AnsiColor::Blue));
|
||||||
|
EXPECT_EQ(Color(1, 2, 3), Color(1, 2, 3));
|
||||||
|
EXPECT_NE(Color(1, 2, 3), Color(1, 2, 4));
|
||||||
|
|
||||||
|
// A named color and its reference value are different colors: the terminal
|
||||||
|
// renders one from the palette and the other exactly.
|
||||||
|
EXPECT_NE(Color(AnsiColor::BrightRed), Color(255, 0, 0));
|
||||||
|
|
||||||
|
// A palette index occupies the same byte as the red channel, so these pairs
|
||||||
|
// hold identical channel bytes and are told apart only by their kind.
|
||||||
|
EXPECT_NE(Color(AnsiColor::Red), Color(1, 0, 0));
|
||||||
|
EXPECT_NE(Color(AnsiColor::Black), Color(0, 0, 0));
|
||||||
|
EXPECT_NE(Color(AnsiColor::Black), Color());
|
||||||
|
EXPECT_NE(Color(0, 0, 0), Color());
|
||||||
|
EXPECT_EQ(Color(), Color());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ColorTest, Unset) {
|
||||||
|
EXPECT_FALSE(Color().is_set());
|
||||||
|
EXPECT_EQ(Color().kind(), Color::Kind::None);
|
||||||
|
|
||||||
|
// Black is a color like any other, however little of it there is.
|
||||||
|
EXPECT_TRUE(Color(AnsiColor::Black).is_set());
|
||||||
|
EXPECT_TRUE(Color(0, 0, 0).is_set());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ColorTest, Print) {
|
||||||
|
EXPECT_EQ(PrintToString(Color(AnsiColor::BrightMagenta)), "BrightMagenta");
|
||||||
|
EXPECT_EQ(PrintToString(Color(0x12, 0xab, 0xff)), "#12abff");
|
||||||
|
EXPECT_EQ(PrintToString(Color()), "None");
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
} // namespace Carbon::Terminal
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
|
||||||
|
// Exceptions. See /LICENSE for license information.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
||||||
|
|
||||||
|
#include "common/terminal/metrics.h"
|
||||||
|
|
||||||
|
#include "common/check.h"
|
||||||
|
#include "llvm/Support/ConvertUTF.h"
|
||||||
|
#include "llvm/Support/Unicode.h"
|
||||||
|
|
||||||
|
namespace Carbon::Terminal {
|
||||||
|
|
||||||
|
// Stands in for anything a UTF-8 terminal has no rendering for: invalid UTF-8,
|
||||||
|
// control characters, and unassigned code points.
|
||||||
|
static constexpr char32_t Utf8Replacement = U'�';
|
||||||
|
|
||||||
|
// Returns whether an ASCII terminal renders `code_point` as itself, in one
|
||||||
|
// column.
|
||||||
|
static auto IsPrintableAscii(char32_t code_point) -> bool {
|
||||||
|
return code_point >= 0x20 && code_point < 0x7f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spelled out rather than handed to a general converter, which walks a range
|
||||||
|
// and checks bounds this already knows. Box-drawing characters go through here
|
||||||
|
// for every cell of every line drawn.
|
||||||
|
//
|
||||||
|
// TODO: Offer this to LLVM, whose `ConvertCodePointToUTF8` is the general
|
||||||
|
// converter this replaces. Encoding one code point at a time is what anything
|
||||||
|
// writing UTF-8 out of a grid does, so this belongs beside it rather than
|
||||||
|
// here; drop this once it is there.
|
||||||
|
auto EncodeUtf8(char32_t code_point, Utf8Storage& storage) -> llvm::StringRef {
|
||||||
|
// Most of what gets rendered is ASCII, and encoding it is a single byte.
|
||||||
|
if (code_point < 0x80) {
|
||||||
|
storage[0] = static_cast<char>(code_point);
|
||||||
|
return llvm::StringRef(storage.data(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Surrogates have no encoding of their own, and nothing past the last code
|
||||||
|
// point has one at all.
|
||||||
|
if (code_point > 0x10ffff || (code_point >= 0xd800 && code_point < 0xe000)) {
|
||||||
|
code_point = Utf8Replacement;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto trailing = [code_point](int shift) {
|
||||||
|
return static_cast<char>(0b1000'0000 | ((code_point >> shift) & 0b11'1111));
|
||||||
|
};
|
||||||
|
if (code_point < 0x800) {
|
||||||
|
storage[0] = static_cast<char>(0b1100'0000 | (code_point >> 6));
|
||||||
|
storage[1] = trailing(0);
|
||||||
|
return llvm::StringRef(storage.data(), 2);
|
||||||
|
}
|
||||||
|
if (code_point < 0x10000) {
|
||||||
|
storage[0] = static_cast<char>(0b1110'0000 | (code_point >> 12));
|
||||||
|
storage[1] = trailing(6);
|
||||||
|
storage[2] = trailing(0);
|
||||||
|
return llvm::StringRef(storage.data(), 3);
|
||||||
|
}
|
||||||
|
storage[0] = static_cast<char>(0b1111'0000 | (code_point >> 18));
|
||||||
|
storage[1] = trailing(12);
|
||||||
|
storage[2] = trailing(6);
|
||||||
|
storage[3] = trailing(0);
|
||||||
|
return llvm::StringRef(storage.data(), 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the columns `code_point` occupies on a UTF-8 terminal: zero for a
|
||||||
|
// combining mark, one or two for one with a glyph of its own, and a
|
||||||
|
// negative value when there is no printable rendering for it.
|
||||||
|
//
|
||||||
|
// TODO: This encodes a code point only for LLVM to decode it again.
|
||||||
|
// `llvm::sys::unicode::charWidth` computes exactly this and is what
|
||||||
|
// `columnWidthUTF8` calls once per code point, but it is file-local to LLVM's
|
||||||
|
// `Unicode.cpp`. Exposing it there would let this call it directly. LLVM's own
|
||||||
|
// contract already says a string's width is the sum of its code points', so
|
||||||
|
// there is nothing in the way of it.
|
||||||
|
static auto Utf8CodePointWidth(char32_t code_point) -> int {
|
||||||
|
// Printable ASCII is one column, and is most of what gets measured. The
|
||||||
|
// general path parses a UTF-8 sequence and searches several code point
|
||||||
|
// range tables, which is far more than this needs.
|
||||||
|
if (IsPrintableAscii(code_point)) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Utf8Storage storage;
|
||||||
|
return llvm::sys::unicode::columnWidthUTF8(EncodeUtf8(code_point, storage));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Removes the first UTF-8 sequence from `text` and returns the code point it
|
||||||
|
// encodes.
|
||||||
|
static auto TakeUtf8CodePoint(llvm::StringRef& text) -> char32_t {
|
||||||
|
const auto* begin = reinterpret_cast<const llvm::UTF8*>(text.data());
|
||||||
|
const auto* pos = begin;
|
||||||
|
llvm::UTF32 code_point = 0;
|
||||||
|
if (llvm::convertUTF8Sequence(&pos, begin + text.size(), &code_point,
|
||||||
|
llvm::strictConversion) != llvm::conversionOK) {
|
||||||
|
text = text.drop_front(1);
|
||||||
|
return Utf8Replacement;
|
||||||
|
}
|
||||||
|
text = text.drop_front(pos - begin);
|
||||||
|
return code_point;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Metrics::TakeCodePoint(llvm::StringRef& text) const -> char32_t {
|
||||||
|
CARBON_CHECK(!text.empty(), "No code point to take.");
|
||||||
|
if (charset_ == Charset::Ascii) {
|
||||||
|
auto byte = static_cast<unsigned char>(text.front());
|
||||||
|
text = text.drop_front();
|
||||||
|
return byte;
|
||||||
|
}
|
||||||
|
return TakeUtf8CodePoint(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Metrics::CodePointWidth(char32_t code_point) const -> int {
|
||||||
|
if (charset_ == Charset::Ascii) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int width = Utf8CodePointWidth(code_point);
|
||||||
|
// A code point with no rendering is drawn as the replacement character, which
|
||||||
|
// takes one column.
|
||||||
|
return width < 0 ? 1 : width;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Metrics::RenderedCodePoint(char32_t code_point) const -> char32_t {
|
||||||
|
// Printable ASCII is most of what gets drawn, and settling it here keeps it
|
||||||
|
// out of the range tables the general answer searches.
|
||||||
|
if (IsPrintableAscii(code_point)) {
|
||||||
|
return code_point;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback if we can't use unicode.
|
||||||
|
if (charset_ == Charset::Ascii) {
|
||||||
|
return U'?';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Which code points have no rendering is what a negative width names as well,
|
||||||
|
// asked directly rather than through a width that has to encode one to
|
||||||
|
// answer.
|
||||||
|
return llvm::sys::unicode::isPrintable(static_cast<int>(code_point))
|
||||||
|
? code_point
|
||||||
|
: Utf8Replacement;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Metrics::Width(llvm::StringRef text) const -> int {
|
||||||
|
// Checked rather than debug-checked: text with one of these in it measures as
|
||||||
|
// though each took one column, which is not what drawing does, and measuring
|
||||||
|
// wrong is invisible in the output. The scan is one more linear pass over
|
||||||
|
// text that is walked linearly anyway.
|
||||||
|
CARBON_CHECK(
|
||||||
|
text.find_first_of("\t\n\r") == llvm::StringRef::npos,
|
||||||
|
"Width is only for text whose width is its code points', but got `{0}`.",
|
||||||
|
text);
|
||||||
|
if (charset_ == Charset::Ascii) {
|
||||||
|
return static_cast<int>(text.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Text that is valid UTF-8 throughout and printable throughout is the common
|
||||||
|
// case, and LLVM measures a whole run of it in one pass. It answers with a
|
||||||
|
// negative value rather than a width when the text holds anything it can't
|
||||||
|
// measure, which is what the walk below is for: each such code point still
|
||||||
|
// takes the one column the replacement character drawn for it will.
|
||||||
|
int width = llvm::sys::unicode::columnWidthUTF8(text);
|
||||||
|
if (width >= 0) {
|
||||||
|
return width;
|
||||||
|
}
|
||||||
|
|
||||||
|
width = 0;
|
||||||
|
while (!text.empty()) {
|
||||||
|
width += CodePointWidth(TakeUtf8CodePoint(text));
|
||||||
|
}
|
||||||
|
return width;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Metrics::TakeColumns(llvm::StringRef& text, int columns) const
|
||||||
|
-> llvm::StringRef {
|
||||||
|
llvm::StringRef rest = text;
|
||||||
|
int taken = 0;
|
||||||
|
while (!rest.empty()) {
|
||||||
|
llvm::StringRef next = rest;
|
||||||
|
int width = CodePointWidth(TakeCodePoint(next));
|
||||||
|
if (taken + width > columns) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
taken += width;
|
||||||
|
rest = next;
|
||||||
|
}
|
||||||
|
llvm::StringRef prefix = text.drop_back(rest.size());
|
||||||
|
text = rest;
|
||||||
|
return prefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Carbon::Terminal
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
|
||||||
|
// Exceptions. See /LICENSE for license information.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
||||||
|
|
||||||
|
#ifndef CARBON_COMMON_TERMINAL_METRICS_H_
|
||||||
|
#define CARBON_COMMON_TERMINAL_METRICS_H_
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cstddef>
|
||||||
|
|
||||||
|
#include "common/terminal/capabilities.h"
|
||||||
|
#include "llvm/ADT/StringRef.h"
|
||||||
|
|
||||||
|
namespace Carbon::Terminal {
|
||||||
|
|
||||||
|
// The most bytes one code point encodes to in UTF-8, and storage for one.
|
||||||
|
inline constexpr size_t MaxUtf8Bytes = 4;
|
||||||
|
using Utf8Storage = std::array<char, MaxUtf8Bytes>;
|
||||||
|
|
||||||
|
// Encodes `code_point` as UTF-8 into `storage`, returning the bytes written.
|
||||||
|
//
|
||||||
|
// Code points with no valid encoding, including surrogates and anything past
|
||||||
|
// U+10FFFF, become the replacement character.
|
||||||
|
auto EncodeUtf8(char32_t code_point, Utf8Storage& storage) -> llvm::StringRef;
|
||||||
|
|
||||||
|
// How many columns a terminal spends on text, given the charset it decodes
|
||||||
|
// with.
|
||||||
|
//
|
||||||
|
// Which bytes make up a column depends on the charset, so every question about
|
||||||
|
// the size of text is a question about the charset as well, and this is what
|
||||||
|
// answers both at once. `Buffer` holds one and lays its cells out with it;
|
||||||
|
// anything deciding where to put something asks one directly rather than
|
||||||
|
// keeping its own idea of how wide a string is.
|
||||||
|
//
|
||||||
|
// Nothing here converts between a byte offset and a column: which byte a column
|
||||||
|
// lands on depends on the encoding, and which column a byte lands in depends on
|
||||||
|
// the width of everything before it. `TakeColumns` hands back the text it cut
|
||||||
|
// rather than an offset into it, so a caller never holds one count where the
|
||||||
|
// other belongs.
|
||||||
|
//
|
||||||
|
// TODO: Every width here is a sum over code points taken in logical order,
|
||||||
|
// which is only the width on screen for left-to-right text. Bidirectional text
|
||||||
|
// reorders, so a run's width still adds up but `TakeColumns` has no meaning:
|
||||||
|
// the prefix occupying the first N columns need not be a prefix of the string.
|
||||||
|
// Settle this together with the question `Buffer`'s own TODO describes, since
|
||||||
|
// both turn on what a client hands over.
|
||||||
|
class Metrics {
|
||||||
|
public:
|
||||||
|
explicit constexpr Metrics(Charset charset) : charset_(charset) {}
|
||||||
|
|
||||||
|
constexpr auto charset() const -> Charset { return charset_; }
|
||||||
|
|
||||||
|
// Removes the next code point from `text`, which must not be empty, and
|
||||||
|
// returns it: one byte under `Charset::Ascii`, and one decoded code point
|
||||||
|
// under `Charset::Utf8`.
|
||||||
|
//
|
||||||
|
// A byte that doesn't start a valid sequence yields the replacement
|
||||||
|
// character and is consumed on its own, so decoding resynchronizes at the
|
||||||
|
// next byte rather than discarding the rest of the text.
|
||||||
|
auto TakeCodePoint(llvm::StringRef& text) const -> char32_t;
|
||||||
|
|
||||||
|
// Returns the columns `code_point` occupies once drawn, which is what drawing
|
||||||
|
// it advances by.
|
||||||
|
//
|
||||||
|
// Under `Charset::Ascii` every code point is one column. Under
|
||||||
|
// `Charset::Utf8` a combining mark is zero, since it renders into the column
|
||||||
|
// before it, and anything with no printable rendering is one, since it is
|
||||||
|
// drawn as a replacement character.
|
||||||
|
//
|
||||||
|
// A combining mark is the only thing zero is ever the answer for, which is
|
||||||
|
// what lets `Buffer` read a zero as one: a code point to fold into the cell
|
||||||
|
// before it rather than give a cell of its own. A code point that takes no
|
||||||
|
// column without combining with anything, such as U+200C ZERO WIDTH
|
||||||
|
// NON-JOINER, has no printable rendering here and takes the column its
|
||||||
|
// replacement character does. Terminals disagree about those -- some give
|
||||||
|
// them a column and some don't -- so drawing one as itself would leave the
|
||||||
|
// columns counted here and the columns painted disagreeing from there on.
|
||||||
|
auto CodePointWidth(char32_t code_point) const -> int;
|
||||||
|
|
||||||
|
// Returns the code point to render for `code_point`, which is a replacement
|
||||||
|
// character where it has no dependable rendering of its own.
|
||||||
|
//
|
||||||
|
// Under `Charset::Ascii` that is anything outside printable ASCII, because a
|
||||||
|
// terminal decoding some single-byte encoding will draw such a byte as
|
||||||
|
// something and there is no way to know what. Under `Charset::Utf8` it is
|
||||||
|
// anything with no printable rendering at all, which includes the surrogates
|
||||||
|
// and so covers everything UTF-8 has no encoding for as well.
|
||||||
|
auto RenderedCodePoint(char32_t code_point) const -> char32_t;
|
||||||
|
|
||||||
|
// Returns the columns `text` occupies once drawn.
|
||||||
|
//
|
||||||
|
// `text` must hold no character that drawing gives a width other than its
|
||||||
|
// code points', so no tab, newline, or carriage return. Those are positional
|
||||||
|
// -- what a tab advances by depends on where the text began -- which makes
|
||||||
|
// them questions about a drawing rather than about the text, and `Buffer`
|
||||||
|
// answers those.
|
||||||
|
auto Width(llvm::StringRef text) const -> int;
|
||||||
|
|
||||||
|
// Removes and returns the longest prefix of `text` that occupies at most
|
||||||
|
// `columns` columns.
|
||||||
|
//
|
||||||
|
// A code point that would straddle the end stops the walk before it, so a cut
|
||||||
|
// never lands inside one and the prefix is never wider than asked for -- it
|
||||||
|
// can be one column narrower, where a double-width character sits on the
|
||||||
|
// boundary.
|
||||||
|
auto TakeColumns(llvm::StringRef& text, int columns) const -> llvm::StringRef;
|
||||||
|
|
||||||
|
private:
|
||||||
|
Charset charset_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Carbon::Terminal
|
||||||
|
|
||||||
|
#endif // CARBON_COMMON_TERMINAL_METRICS_H_
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
|
||||||
|
// Exceptions. See /LICENSE for license information.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
||||||
|
|
||||||
|
#include "common/terminal/metrics.h"
|
||||||
|
|
||||||
|
#include <gmock/gmock.h>
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
#include "llvm/ADT/StringRef.h"
|
||||||
|
|
||||||
|
namespace Carbon::Terminal {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// "e" followed by U+0301 COMBINING ACUTE ACCENT, which is one column because
|
||||||
|
// the mark renders into the column the "e" is in.
|
||||||
|
static constexpr llvm::StringLiteral AcuteE = "é";
|
||||||
|
|
||||||
|
TEST(MetricsTest, Width) {
|
||||||
|
Metrics utf8(Charset::Utf8);
|
||||||
|
EXPECT_EQ(utf8.Width(""), 0);
|
||||||
|
EXPECT_EQ(utf8.Width("hello"), 5);
|
||||||
|
EXPECT_EQ(utf8.Width("中中"), 4);
|
||||||
|
EXPECT_EQ(utf8.Width("a中b"), 4);
|
||||||
|
EXPECT_EQ(utf8.Width(AcuteE), 1);
|
||||||
|
|
||||||
|
// Every byte is a column when the terminal isn't decoding UTF-8.
|
||||||
|
Metrics ascii(Charset::Ascii);
|
||||||
|
EXPECT_EQ(ascii.Width("hello"), 5);
|
||||||
|
EXPECT_EQ(ascii.Width("中中"), 6);
|
||||||
|
EXPECT_EQ(ascii.Width(AcuteE), 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(MetricsTest, CodePointWidth) {
|
||||||
|
Metrics utf8(Charset::Utf8);
|
||||||
|
EXPECT_EQ(utf8.CodePointWidth(U'a'), 1);
|
||||||
|
EXPECT_EQ(utf8.CodePointWidth(U'中'), 2);
|
||||||
|
// A combining mark renders into the column before it.
|
||||||
|
EXPECT_EQ(utf8.CodePointWidth(U'́'), 0);
|
||||||
|
// Something with no rendering is drawn as a replacement, which is a column.
|
||||||
|
EXPECT_EQ(utf8.CodePointWidth(U''), 1);
|
||||||
|
|
||||||
|
Metrics ascii(Charset::Ascii);
|
||||||
|
EXPECT_EQ(ascii.CodePointWidth(U'a'), 1);
|
||||||
|
EXPECT_EQ(ascii.CodePointWidth(U'中'), 1);
|
||||||
|
EXPECT_EQ(ascii.CodePointWidth(U'́'), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(MetricsTest, OnlyCombiningMarksAreZeroColumns) {
|
||||||
|
// Drawing reads a width of zero as "renders into the cell before this one",
|
||||||
|
// so a code point that takes no column without combining with anything has to
|
||||||
|
// measure as something else. Terminals disagree about these -- Terminal.app
|
||||||
|
// gives U+200C a column and VS Code's terminal gives it none -- so each is
|
||||||
|
// drawn as a replacement character, which takes exactly one.
|
||||||
|
Metrics utf8(Charset::Utf8);
|
||||||
|
for (char32_t code_point : {U'\u200b', U'\u200c', U'\u200d', U'\ufeff'}) {
|
||||||
|
EXPECT_EQ(utf8.CodePointWidth(code_point), 1)
|
||||||
|
<< static_cast<uint32_t>(code_point);
|
||||||
|
EXPECT_EQ(utf8.RenderedCodePoint(code_point), U'�')
|
||||||
|
<< static_cast<uint32_t>(code_point);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(MetricsTest, RenderedCodePoint) {
|
||||||
|
Metrics utf8(Charset::Utf8);
|
||||||
|
EXPECT_EQ(utf8.RenderedCodePoint(U'a'), U'a');
|
||||||
|
EXPECT_EQ(utf8.RenderedCodePoint(U'中'), U'中');
|
||||||
|
EXPECT_EQ(utf8.RenderedCodePoint(U''), U'�');
|
||||||
|
|
||||||
|
// Code points that UTF-8 has no encoding for have no rendering either.
|
||||||
|
EXPECT_EQ(utf8.RenderedCodePoint(static_cast<char32_t>(0xd800)), U'�');
|
||||||
|
EXPECT_EQ(utf8.RenderedCodePoint(static_cast<char32_t>(0x110000)), U'�');
|
||||||
|
|
||||||
|
// An ASCII terminal is only given what it draws as itself, because there is
|
||||||
|
// no telling what it would draw for anything else.
|
||||||
|
Metrics ascii(Charset::Ascii);
|
||||||
|
EXPECT_EQ(ascii.RenderedCodePoint(U'a'), U'a');
|
||||||
|
EXPECT_EQ(ascii.RenderedCodePoint(U'中'), U'?');
|
||||||
|
EXPECT_EQ(ascii.RenderedCodePoint(U''), U'?');
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(MetricsTest, TakeColumns) {
|
||||||
|
Metrics utf8(Charset::Utf8);
|
||||||
|
llvm::StringRef text = "abcde";
|
||||||
|
EXPECT_EQ(utf8.TakeColumns(text, 3), "abc");
|
||||||
|
EXPECT_EQ(text, "de");
|
||||||
|
|
||||||
|
// Taking more than there is takes all of it.
|
||||||
|
EXPECT_EQ(utf8.TakeColumns(text, 10), "de");
|
||||||
|
EXPECT_EQ(text, "");
|
||||||
|
|
||||||
|
// Taking nothing takes nothing, and a negative width is no different.
|
||||||
|
text = "abcde";
|
||||||
|
EXPECT_EQ(utf8.TakeColumns(text, 0), "");
|
||||||
|
EXPECT_EQ(utf8.TakeColumns(text, -1), "");
|
||||||
|
EXPECT_EQ(text, "abcde");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(MetricsTest, TakeColumnsKeepsWideCharactersWhole) {
|
||||||
|
Metrics utf8(Charset::Utf8);
|
||||||
|
// A character that would straddle the end stops the walk before it, so the
|
||||||
|
// prefix comes back a column short rather than half a character wide.
|
||||||
|
llvm::StringRef text = "中中中";
|
||||||
|
llvm::StringRef prefix = utf8.TakeColumns(text, 3);
|
||||||
|
EXPECT_EQ(prefix, "中");
|
||||||
|
EXPECT_EQ(utf8.Width(prefix), 2);
|
||||||
|
EXPECT_EQ(text, "中中");
|
||||||
|
|
||||||
|
// A request landing on a character boundary takes the whole prefix.
|
||||||
|
text = "中中中";
|
||||||
|
EXPECT_EQ(utf8.TakeColumns(text, 4), "中中");
|
||||||
|
EXPECT_EQ(text, "中");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(MetricsTest, TakeColumnsUnderAscii) {
|
||||||
|
// Every byte is a column, so a multi-byte character is cut like any other
|
||||||
|
// run of bytes.
|
||||||
|
Metrics ascii(Charset::Ascii);
|
||||||
|
llvm::StringRef text = "中";
|
||||||
|
EXPECT_EQ(ascii.TakeColumns(text, 2).size(), 2U);
|
||||||
|
EXPECT_EQ(text.size(), 1U);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(MetricsTest, TakeCodePointResynchronizesOnInvalidUtf8) {
|
||||||
|
Metrics utf8(Charset::Utf8);
|
||||||
|
// A byte that starts no valid sequence is consumed on its own, so the text
|
||||||
|
// after it is still decoded rather than being discarded.
|
||||||
|
llvm::StringRef text =
|
||||||
|
"\xff"
|
||||||
|
"a";
|
||||||
|
EXPECT_EQ(utf8.TakeCodePoint(text), U'�');
|
||||||
|
EXPECT_EQ(utf8.TakeCodePoint(text), U'a');
|
||||||
|
EXPECT_TRUE(text.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(MetricsTest, EncodeUtf8) {
|
||||||
|
Utf8Storage storage;
|
||||||
|
EXPECT_EQ(EncodeUtf8(U'a', storage), "a");
|
||||||
|
EXPECT_EQ(EncodeUtf8(U'é', storage), "é");
|
||||||
|
EXPECT_EQ(EncodeUtf8(U'中', storage), "中");
|
||||||
|
EXPECT_EQ(EncodeUtf8(U'\U0001f525', storage), "\U0001f525");
|
||||||
|
|
||||||
|
// A code point with no encoding of its own becomes the replacement.
|
||||||
|
EXPECT_EQ(EncodeUtf8(static_cast<char32_t>(0xd800), storage), "�");
|
||||||
|
EXPECT_EQ(EncodeUtf8(static_cast<char32_t>(0x110000), storage), "�");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(MetricsDeathTest, WidthRejectsPositionalCharacters) {
|
||||||
|
// A tab's width is a fact about a drawing rather than about the text, so
|
||||||
|
// answering for one here would be answering a question this can't see the
|
||||||
|
// inputs to.
|
||||||
|
Metrics metrics(Charset::Utf8);
|
||||||
|
EXPECT_DEATH((void)metrics.Width("a\tb"), "Width is only for text whose");
|
||||||
|
EXPECT_DEATH((void)metrics.Width("a\nb"), "Width is only for text whose");
|
||||||
|
EXPECT_DEATH((void)metrics.Width("a\rb"), "Width is only for text whose");
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
} // namespace Carbon::Terminal
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
|
||||||
|
// Exceptions. See /LICENSE for license information.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
||||||
|
|
||||||
|
#ifndef CARBON_COMMON_TERMINAL_OUTPUT_BUFFER_REF_H_
|
||||||
|
#define CARBON_COMMON_TERMINAL_OUTPUT_BUFFER_REF_H_
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <concepts>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
#include "llvm/ADT/SmallVector.h"
|
||||||
|
#include "llvm/ADT/StringRef.h"
|
||||||
|
|
||||||
|
namespace Carbon::Terminal {
|
||||||
|
|
||||||
|
// A reference to the buffer a terminal rendering is assembled into.
|
||||||
|
//
|
||||||
|
// This owns nothing. It refers to a buffer the caller holds, which must outlive
|
||||||
|
// it, and converts implicitly from one so that rendering goes into storage the
|
||||||
|
// caller already has.
|
||||||
|
//
|
||||||
|
// Rendering assembles bytes here rather than streaming them: a stream call per
|
||||||
|
// literal and per number costs measurably more than handing over a finished
|
||||||
|
// sequence, and code that wants a stream prints the buffer once it is complete.
|
||||||
|
//
|
||||||
|
// Appending is shaped around what terminal output is made of, which is a great
|
||||||
|
// many short escape sequences, each a handful of literal bytes around a number
|
||||||
|
// that never exceeds 255. Taking a whole sequence at a time grows the buffer
|
||||||
|
// once per sequence rather than once per byte, and that difference is much of
|
||||||
|
// what rendering costs.
|
||||||
|
class OutputBufferRef {
|
||||||
|
public:
|
||||||
|
// Implicit, so that call sites pass the buffer they already hold rather than
|
||||||
|
// naming this type.
|
||||||
|
//
|
||||||
|
// NOLINTNEXTLINE(google-explicit-constructor)
|
||||||
|
OutputBufferRef(llvm::SmallVectorImpl<char>& bytes) : bytes_(&bytes) {}
|
||||||
|
|
||||||
|
// Appends `pieces`, each of which is either text, appended as it is, or a
|
||||||
|
// `uint8_t`, appended in decimal.
|
||||||
|
//
|
||||||
|
// No other type is accepted, so the two can never be taken for each other,
|
||||||
|
// and nothing needs one: the literal bytes of an escape sequence are always
|
||||||
|
// text, and every number one carries is a channel value, a palette index, or
|
||||||
|
// an SGR code, none of which exceed 255.
|
||||||
|
//
|
||||||
|
// No piece may point into the buffer, which appending can reallocate.
|
||||||
|
template <typename... PieceT>
|
||||||
|
auto Append(const PieceT&... pieces) -> void {
|
||||||
|
if constexpr (sizeof...(pieces) == 1) {
|
||||||
|
// A lone piece has nothing to assemble, and the buffer's own append is
|
||||||
|
// already the single growth and single copy this is after.
|
||||||
|
(AppendPiece(pieces), ...);
|
||||||
|
} else {
|
||||||
|
// Growing to the bound before writing keeps how far the buffer grows
|
||||||
|
// independent of the piece values, so computing one can't hold that up.
|
||||||
|
// Only the trim afterwards depends on how many digits a number took.
|
||||||
|
size_t begin = bytes_->size();
|
||||||
|
bytes_->resize_for_overwrite(begin + (AppendedSize(pieces) + ... + 0));
|
||||||
|
char* data = bytes_->data();
|
||||||
|
char* cursor = data + begin;
|
||||||
|
((cursor = WritePiece(cursor, pieces)), ...);
|
||||||
|
bytes_->truncate(cursor - data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
// The room a number needs: its three digits, plus one more because it is
|
||||||
|
// written as a single four-byte store whose last byte is discarded.
|
||||||
|
static constexpr size_t NumberBytes = 4;
|
||||||
|
|
||||||
|
// The decimal text of a number, and how many digits it took. The digits are
|
||||||
|
// at the front and the length in the byte after them, so a whole entry is one
|
||||||
|
// store and the length says how far of it to keep.
|
||||||
|
struct NumberText {
|
||||||
|
std::array<char, NumberBytes - 1> digits;
|
||||||
|
uint8_t length;
|
||||||
|
};
|
||||||
|
static_assert(sizeof(NumberText) == NumberBytes,
|
||||||
|
"A number is written by storing a whole entry at once.");
|
||||||
|
|
||||||
|
// The text of every value a number piece can hold. A kilobyte of table, in
|
||||||
|
// exchange for a lookup where computing the digits would branch on the value
|
||||||
|
// three times.
|
||||||
|
static constexpr std::array<NumberText, 256> NumberTexts = [] {
|
||||||
|
std::array<NumberText, 256> texts = {};
|
||||||
|
for (int value = 0; value < 256; ++value) {
|
||||||
|
NumberText& text = texts[value];
|
||||||
|
text.length = 1 + (value >= 10) + (value >= 100);
|
||||||
|
int rest = value;
|
||||||
|
for (int digit = text.length; digit > 0; --digit) {
|
||||||
|
text.digits[digit - 1] = static_cast<char>('0' + rest % 10);
|
||||||
|
rest /= 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return texts;
|
||||||
|
}();
|
||||||
|
|
||||||
|
// Returns the most bytes a piece can append. A number contributes the bound
|
||||||
|
// above rather than the digits it will take, so the bound for a sequence
|
||||||
|
// doesn't depend on any of the values in it.
|
||||||
|
template <size_t N>
|
||||||
|
static constexpr auto AppendedSize(const char (& /*piece*/)[N]) -> size_t {
|
||||||
|
return N - 1;
|
||||||
|
}
|
||||||
|
static constexpr auto AppendedSize(llvm::StringRef piece) -> size_t {
|
||||||
|
return piece.size();
|
||||||
|
}
|
||||||
|
template <std::same_as<uint8_t> T>
|
||||||
|
static constexpr auto AppendedSize(T /*piece*/) -> size_t {
|
||||||
|
return NumberBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Writes a piece at `out` and returns the position past it. There must be
|
||||||
|
// `AppendedSize(piece)` bytes of room, as nothing here checks.
|
||||||
|
template <size_t N>
|
||||||
|
static auto WritePiece(char* out, const char (&piece)[N]) -> char* {
|
||||||
|
std::memcpy(out, piece, N - 1);
|
||||||
|
return out + N - 1;
|
||||||
|
}
|
||||||
|
static auto WritePiece(char* out, llvm::StringRef piece) -> char* {
|
||||||
|
// An empty `StringRef` may hold a null pointer, which `memcpy` doesn't
|
||||||
|
// accept even for an empty copy.
|
||||||
|
if (!piece.empty()) {
|
||||||
|
std::memcpy(out, piece.data(), piece.size());
|
||||||
|
}
|
||||||
|
return out + piece.size();
|
||||||
|
}
|
||||||
|
template <std::same_as<uint8_t> T>
|
||||||
|
static auto WritePiece(char* out, T piece) -> char* {
|
||||||
|
// One load and one store, with no branch on the value. Escape sequences
|
||||||
|
// carry color channels and palette indices, which are spread across the
|
||||||
|
// whole range, so a branch per digit is one the processor can't predict,
|
||||||
|
// and there are four numbers in a truecolor escape. The store always covers
|
||||||
|
// four bytes, which is why a number reserves that many, and the cursor
|
||||||
|
// advances only over the digits that count.
|
||||||
|
const NumberText& text = NumberTexts[piece];
|
||||||
|
std::memcpy(out, &text, sizeof(text));
|
||||||
|
return out + text.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Appends a piece on its own, growing the buffer to fit it.
|
||||||
|
template <size_t N>
|
||||||
|
auto AppendPiece(const char (&piece)[N]) -> void {
|
||||||
|
bytes_->append(piece, piece + N - 1);
|
||||||
|
}
|
||||||
|
auto AppendPiece(llvm::StringRef piece) -> void {
|
||||||
|
bytes_->append(piece.begin(), piece.end());
|
||||||
|
}
|
||||||
|
template <std::same_as<uint8_t> T>
|
||||||
|
auto AppendPiece(T piece) -> void {
|
||||||
|
std::array<char, NumberBytes> digits;
|
||||||
|
bytes_->append(digits.data(), WritePiece(digits.data(), piece));
|
||||||
|
}
|
||||||
|
|
||||||
|
llvm::SmallVectorImpl<char>* bytes_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Carbon::Terminal
|
||||||
|
|
||||||
|
#endif // CARBON_COMMON_TERMINAL_OUTPUT_BUFFER_REF_H_
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
|
||||||
|
// Exceptions. See /LICENSE for license information.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
||||||
|
|
||||||
|
#include "common/terminal/output_buffer_ref.h"
|
||||||
|
|
||||||
|
#include <gmock/gmock.h>
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include "llvm/ADT/SmallString.h"
|
||||||
|
|
||||||
|
namespace Carbon::Terminal {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using ::testing::Eq;
|
||||||
|
|
||||||
|
// A single piece and several pieces are appended by different code, so both
|
||||||
|
// appear throughout these tests rather than in one case of their own.
|
||||||
|
|
||||||
|
TEST(OutputBufferRefTest, Text) {
|
||||||
|
llvm::SmallString<16> bytes;
|
||||||
|
OutputBufferRef out = bytes;
|
||||||
|
out.Append("one");
|
||||||
|
out.Append(llvm::StringRef(" two"));
|
||||||
|
out.Append(std::string(" three"));
|
||||||
|
out.Append(" four", llvm::StringRef(" five"));
|
||||||
|
EXPECT_THAT(bytes, Eq("one two three four five"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(OutputBufferRefTest, EmptyPieces) {
|
||||||
|
llvm::SmallString<16> bytes;
|
||||||
|
OutputBufferRef out = bytes;
|
||||||
|
out.Append();
|
||||||
|
out.Append("");
|
||||||
|
out.Append("", llvm::StringRef(), "kept", llvm::StringRef(""));
|
||||||
|
EXPECT_THAT(bytes, Eq("kept"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(OutputBufferRefTest, NumbersUseEveryDigitCount) {
|
||||||
|
llvm::SmallString<16> bytes;
|
||||||
|
OutputBufferRef out = bytes;
|
||||||
|
for (uint8_t value : {0, 9, 10, 99, 100, 255}) {
|
||||||
|
out.Append(value);
|
||||||
|
out.Append(" ", value, " ");
|
||||||
|
}
|
||||||
|
EXPECT_THAT(bytes, Eq("0 0 9 9 10 10 99 99 100 100 255 255 "));
|
||||||
|
}
|
||||||
|
|
||||||
|
// A number always writes fewer bytes than it reserves, so pieces after one in
|
||||||
|
// the same call are what catch a misplaced write.
|
||||||
|
TEST(OutputBufferRefTest, NumbersFollowedByMorePieces) {
|
||||||
|
llvm::SmallString<32> bytes;
|
||||||
|
OutputBufferRef out = bytes;
|
||||||
|
out.Append("\x1b[", static_cast<uint8_t>(38), ";2;", static_cast<uint8_t>(1),
|
||||||
|
";", static_cast<uint8_t>(22), ";", static_cast<uint8_t>(255),
|
||||||
|
"m");
|
||||||
|
EXPECT_THAT(bytes, Eq("\x1b[38;2;1;22;255m"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(OutputBufferRefTest, AppendsAfterExistingContents) {
|
||||||
|
llvm::SmallString<16> bytes = llvm::StringRef("before:");
|
||||||
|
OutputBufferRef out = bytes;
|
||||||
|
out.Append(static_cast<uint8_t>(7));
|
||||||
|
EXPECT_THAT(bytes, Eq("before:7"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Appending has to work the same however the buffer is laid out, and a number
|
||||||
|
// leaves the buffer grown further than it wrote, so reallocation is where a
|
||||||
|
// size mistake would show up.
|
||||||
|
TEST(OutputBufferRefTest, AppendsPastInlineCapacity) {
|
||||||
|
llvm::SmallString<8> bytes;
|
||||||
|
OutputBufferRef out = bytes;
|
||||||
|
std::string expected;
|
||||||
|
for (int i = 0; i < 100; ++i) {
|
||||||
|
out.Append("x", static_cast<uint8_t>(i));
|
||||||
|
expected += "x" + std::to_string(i);
|
||||||
|
}
|
||||||
|
EXPECT_THAT(bytes, Eq(expected));
|
||||||
|
EXPECT_THAT(bytes.size(), Eq(expected.size()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// References to one buffer all append to it, and none of them own it, so the
|
||||||
|
// buffer keeps everything written through any of them.
|
||||||
|
TEST(OutputBufferRefTest, ReferencesShareTheirBuffer) {
|
||||||
|
llvm::SmallString<16> bytes;
|
||||||
|
OutputBufferRef first = bytes;
|
||||||
|
first.Append("a");
|
||||||
|
{
|
||||||
|
OutputBufferRef second = bytes;
|
||||||
|
second.Append("b");
|
||||||
|
}
|
||||||
|
OutputBufferRef copy = first;
|
||||||
|
copy.Append("c");
|
||||||
|
first.Append("d");
|
||||||
|
EXPECT_THAT(bytes, Eq("abcd"));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
} // namespace Carbon::Terminal
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
|
||||||
|
// Exceptions. See /LICENSE for license information.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
||||||
|
|
||||||
|
// Adversarial inputs across the terminal library's surface.
|
||||||
|
//
|
||||||
|
// The library draws whatever a source file contains, for a terminal whose width
|
||||||
|
// came from an environment variable. Both are things a user controls, and
|
||||||
|
// neither may crash, read out of bounds, or quietly produce a position that is
|
||||||
|
// wrong. The tests here feed each entry point the inputs most likely to do one
|
||||||
|
// of those, and assert that what comes back is coherent rather than asserting
|
||||||
|
// any particular rendering.
|
||||||
|
//
|
||||||
|
// What is deliberately not here: anything a caller is checked for getting
|
||||||
|
// wrong, which is text past `MaxTextBytes` and coordinates outside the width
|
||||||
|
// laid out for. Those are death tests in `buffer_test`. What remains is the
|
||||||
|
// text and the width, neither of which a caller can validate ahead of drawing.
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <limits>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "common/terminal/buffer.h"
|
||||||
|
#include "common/terminal/capabilities.h"
|
||||||
|
#include "common/terminal/metrics.h"
|
||||||
|
#include "common/terminal/style.h"
|
||||||
|
#include "llvm/ADT/SmallString.h"
|
||||||
|
#include "llvm/ADT/StringRef.h"
|
||||||
|
|
||||||
|
namespace Carbon::Terminal {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Byte sequences that stress column accounting.
|
||||||
|
auto HostileText() -> std::vector<std::string> {
|
||||||
|
return {
|
||||||
|
"",
|
||||||
|
"\x01\x02\x7f", // C0 controls and delete
|
||||||
|
"\xff\xfe\xfd", // never valid UTF-8
|
||||||
|
"\xe4\xb8", // truncated multi-byte sequence
|
||||||
|
"\xe4\xb8\x96\xe4\xb8", // valid then truncated
|
||||||
|
"\xcc\x81", // combining mark with no base
|
||||||
|
"e\xcc\x81\xcc\x82\xcc\x83", // a base with several marks
|
||||||
|
"\xf0\x9f\x94\xa5", // outside the basic plane
|
||||||
|
"\xed\xa0\x80", // a surrogate, which UTF-8 forbids
|
||||||
|
"\xc0\x80", // overlong encoding of NUL
|
||||||
|
"中中中", // double-width throughout
|
||||||
|
"a\tb\nc\r\nd", // every positional character
|
||||||
|
"\t\t\t\t\t\t\t\t", // nothing but tabs
|
||||||
|
"\n\n\n", // nothing but newlines
|
||||||
|
std::string(4096, ' '), // a long run of blanks
|
||||||
|
std::string(1024, '\t'), // a long run of tabs
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Renders `buffer` and checks the bytes are coherent. Everything here draws
|
||||||
|
// with the default style, so what a style leaves behind is `buffer_test`'s to
|
||||||
|
// cover; this is about the text surviving at all.
|
||||||
|
auto RenderAndCheck(const Buffer& buffer, ColorMode mode) -> std::string {
|
||||||
|
llvm::SmallString<256> out;
|
||||||
|
buffer.Render(out, mode);
|
||||||
|
std::string rendered(out);
|
||||||
|
if (rendered.empty()) {
|
||||||
|
return rendered;
|
||||||
|
}
|
||||||
|
EXPECT_EQ(rendered.back(), '\n');
|
||||||
|
return rendered;
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(PressureTest, DrawTextSurvivesHostileBytes) {
|
||||||
|
for (Charset charset : {Charset::Ascii, Charset::Utf8}) {
|
||||||
|
for (const std::string& text : HostileText()) {
|
||||||
|
for (int x : {0, 1, 7}) {
|
||||||
|
Buffer buffer(8, charset);
|
||||||
|
Buffer::DrawEnd end = buffer.DrawText(x, 0, text, Style());
|
||||||
|
|
||||||
|
// The end is where drawing would carry on, which is not always a cell
|
||||||
|
// that exists: text ending in a newline leaves it on a row nothing was
|
||||||
|
// drawn on. Nor does the width follow from it, since the grid grows by
|
||||||
|
// halves and overshoots. What must hold is that the end names a
|
||||||
|
// non-negative cell, and that no row was created past where drawing
|
||||||
|
// ended.
|
||||||
|
EXPECT_GE(end.y, 0) << text;
|
||||||
|
EXPECT_LE(buffer.height(), end.y + 1) << text;
|
||||||
|
EXPECT_GE(end.x, 0) << text;
|
||||||
|
|
||||||
|
// Measuring answers what drawing did.
|
||||||
|
EXPECT_EQ(buffer.MeasureText(x, 0, text), end) << text;
|
||||||
|
|
||||||
|
RenderAndCheck(buffer, ColorMode::Ansi16);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(PressureTest, WrappedTextSurvivesHostileBytes) {
|
||||||
|
for (Charset charset : {Charset::Ascii, Charset::Utf8}) {
|
||||||
|
for (const std::string& text : HostileText()) {
|
||||||
|
// A width of one is the tightest anything can be asked to wrap into.
|
||||||
|
for (int width : {1, 2, 3, 80}) {
|
||||||
|
Buffer buffer(width, charset);
|
||||||
|
Buffer::DrawEnd end =
|
||||||
|
buffer.DrawWrappedText(0, 0, 0, width, text, Style());
|
||||||
|
|
||||||
|
EXPECT_GE(end.y, 0) << text;
|
||||||
|
EXPECT_LE(buffer.height(), end.y + 1) << text;
|
||||||
|
EXPECT_GE(end.x, 0) << text;
|
||||||
|
EXPECT_EQ(buffer.MeasureWrappedText(0, 0, 0, width, text), end) << text;
|
||||||
|
// The width wrapping this wouldn't overhang is a fact about the text
|
||||||
|
// rather than about the block it was drawn into.
|
||||||
|
EXPECT_GE(buffer.MeasureWrapWidth(text), 0) << text;
|
||||||
|
|
||||||
|
RenderAndCheck(buffer, ColorMode::Truecolor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(PressureTest, MetricsSurviveHostileBytes) {
|
||||||
|
for (Charset charset : {Charset::Ascii, Charset::Utf8}) {
|
||||||
|
Metrics metrics(charset);
|
||||||
|
for (const std::string& text : HostileText()) {
|
||||||
|
// `Width` requires text with no positional characters in it, which is
|
||||||
|
// checked, so only the rest is measured here.
|
||||||
|
if (llvm::StringRef(text).find_first_of("\t\n\r") ==
|
||||||
|
llvm::StringRef::npos) {
|
||||||
|
int width = metrics.Width(text);
|
||||||
|
EXPECT_GE(width, 0) << text;
|
||||||
|
|
||||||
|
// Cutting at any column gives back a prefix that is no wider than
|
||||||
|
// asked for and that leaves the rest of the string behind it.
|
||||||
|
for (int columns : {-1, 0, 1, 2, width, width + 1}) {
|
||||||
|
llvm::StringRef rest = text;
|
||||||
|
llvm::StringRef prefix = metrics.TakeColumns(rest, columns);
|
||||||
|
EXPECT_EQ(prefix.size() + rest.size(), text.size()) << text;
|
||||||
|
EXPECT_LE(metrics.Width(prefix), std::max(columns, 0)) << text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Taking code points consumes the whole string however invalid it is,
|
||||||
|
// rather than stalling on a byte it can't decode.
|
||||||
|
llvm::StringRef rest = text;
|
||||||
|
size_t steps = 0;
|
||||||
|
while (!rest.empty()) {
|
||||||
|
metrics.TakeCodePoint(rest);
|
||||||
|
++steps;
|
||||||
|
ASSERT_LE(steps, text.size()) << "TakeCodePoint failed to consume";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(PressureTest, OverlappingDrawsLeaveNoHalfCharacters) {
|
||||||
|
// Double-width characters, lines, and text all writing over each other is
|
||||||
|
// where a stale continuation cell would show up as a rendering with half a
|
||||||
|
// character in it.
|
||||||
|
Buffer buffer(8, Charset::Utf8);
|
||||||
|
for (int pass = 0; pass < 3; ++pass) {
|
||||||
|
buffer.DrawText(0, 0, "中中中中", Style());
|
||||||
|
buffer.DrawHorizontalLine(1, 0, 3, Style());
|
||||||
|
buffer.DrawText(2, 0, "中", Style());
|
||||||
|
buffer.DrawVerticalLine(3, 0, 2, Style());
|
||||||
|
buffer.DrawCodePoint(4, 0, U'中', Style());
|
||||||
|
buffer.DrawCodePoint(5, 0, U'x', Style());
|
||||||
|
buffer.DrawText(0, 0, "ab", Style());
|
||||||
|
}
|
||||||
|
|
||||||
|
// `Render` encodes every cell it emits, so this checks the overdraws leave a
|
||||||
|
// grid that renders at all rather than that no half character survived.
|
||||||
|
std::string rendered = RenderAndCheck(buffer, ColorMode::NoColor);
|
||||||
|
Metrics metrics(Charset::Utf8);
|
||||||
|
llvm::StringRef rest = rendered;
|
||||||
|
while (!rest.empty()) {
|
||||||
|
EXPECT_NE(metrics.TakeCodePoint(rest), 0xfffd) << rendered;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(PressureTest, CapabilitiesWidthNeverBreaksTheBuffer) {
|
||||||
|
// A width claimed by the environment can be anything at all.
|
||||||
|
for (int columns :
|
||||||
|
{-1, 0, 1, 2, 80, Buffer::MaxColumns - 1, Buffer::MaxColumns,
|
||||||
|
Buffer::MaxColumns + 1, 1 << 20, std::numeric_limits<int>::max()}) {
|
||||||
|
Capabilities capabilities = {.charset = Charset::Utf8, .columns = columns};
|
||||||
|
Buffer buffer(capabilities);
|
||||||
|
// Whatever was claimed, what comes out is a width that can be laid out for
|
||||||
|
// and drawn into.
|
||||||
|
EXPECT_GE(buffer.columns(), 1) << columns;
|
||||||
|
EXPECT_LE(buffer.columns(), Buffer::MaxColumns) << columns;
|
||||||
|
buffer.DrawText(0, 0, "中x", Style());
|
||||||
|
RenderAndCheck(buffer, ColorMode::Ansi256);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
} // namespace Carbon::Terminal
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
|
||||||
|
// Exceptions. See /LICENSE for license information.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
||||||
|
|
||||||
|
#include "common/terminal/style.h"
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
|
||||||
|
#include "common/check.h"
|
||||||
|
#include "llvm/ADT/SmallString.h"
|
||||||
|
#include "llvm/ADT/StringExtras.h"
|
||||||
|
|
||||||
|
namespace Carbon::Terminal {
|
||||||
|
|
||||||
|
// Returns the SGR parameter selecting `shape`, or an empty string for `None`.
|
||||||
|
//
|
||||||
|
// The shaped underlines are colon-separated subparameters of the plain
|
||||||
|
// underline code. Terminals that predate them are also the ones limited to the
|
||||||
|
// 16 ANSI colors, and they mishandle the subparameters rather than ignoring
|
||||||
|
// them, so in that mode every shape degrades to a plain underline.
|
||||||
|
static auto UnderlineSgrParam(UnderlineShape shape, ColorMode mode)
|
||||||
|
-> llvm::StringRef {
|
||||||
|
if (mode == ColorMode::Ansi16 && shape != UnderlineShape::None) {
|
||||||
|
return "4";
|
||||||
|
}
|
||||||
|
switch (shape) {
|
||||||
|
case UnderlineShape::None:
|
||||||
|
return "";
|
||||||
|
case UnderlineShape::Single:
|
||||||
|
return "4";
|
||||||
|
case UnderlineShape::Double:
|
||||||
|
return "4:2";
|
||||||
|
case UnderlineShape::Curly:
|
||||||
|
return "4:3";
|
||||||
|
case UnderlineShape::Dotted:
|
||||||
|
return "4:4";
|
||||||
|
case UnderlineShape::Dashed:
|
||||||
|
return "4:5";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Style::NeedsResetFrom(const Style& from) const -> bool {
|
||||||
|
auto drops = [](bool from_set, bool to_set) { return from_set && !to_set; };
|
||||||
|
return drops(from.bold_, bold_) || drops(from.dim_, dim_) ||
|
||||||
|
drops(from.italic_, italic_) || drops(from.reverse_, reverse_) ||
|
||||||
|
drops(from.strikethrough_, strikethrough_) ||
|
||||||
|
drops(from.underline(), underline()) ||
|
||||||
|
drops(from.foreground_.is_set(), foreground_.is_set()) ||
|
||||||
|
drops(from.background_.is_set(), background_.is_set()) ||
|
||||||
|
drops(from.underline_color_.is_set(), underline_color_.is_set());
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Style::AppendDiff(OutputBufferRef out, ColorMode mode,
|
||||||
|
const Style& from) const -> void {
|
||||||
|
CARBON_CHECK(!NeedsResetFrom(from),
|
||||||
|
"Cannot reach this style from `from` without a reset.");
|
||||||
|
|
||||||
|
// Attributes combine into a single SGR sequence, in ascending code order.
|
||||||
|
// Every write goes through `add` so the bound is checked in one place.
|
||||||
|
std::array<llvm::StringRef, 6> params;
|
||||||
|
int param_count = 0;
|
||||||
|
auto add = [&](llvm::StringRef param) {
|
||||||
|
CARBON_CHECK(param_count < static_cast<int>(params.size()),
|
||||||
|
"More SGR parameters than the {0} there is room for.",
|
||||||
|
params.size());
|
||||||
|
params[param_count++] = param;
|
||||||
|
};
|
||||||
|
auto add_if = [&](bool from_set, bool to_set, llvm::StringRef param) {
|
||||||
|
if (to_set && !from_set) {
|
||||||
|
add(param);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
add_if(from.bold_, bold_, "1");
|
||||||
|
add_if(from.dim_, dim_, "2");
|
||||||
|
add_if(from.italic_, italic_, "3");
|
||||||
|
llvm::StringRef underline_param = UnderlineSgrParam(underline_shape_, mode);
|
||||||
|
if (underline_param != UnderlineSgrParam(from.underline_shape_, mode)) {
|
||||||
|
// Turning an underline off needs a reset, which is the caller's to do, so
|
||||||
|
// reaching here with nothing to select would emit an empty parameter.
|
||||||
|
CARBON_CHECK(!underline_param.empty(),
|
||||||
|
"Removing an underline cannot be done with a diff.");
|
||||||
|
add(underline_param);
|
||||||
|
}
|
||||||
|
add_if(from.reverse_, reverse_, "7");
|
||||||
|
add_if(from.strikethrough_, strikethrough_, "9");
|
||||||
|
|
||||||
|
if (param_count > 0) {
|
||||||
|
out.Append("\x1b[", params[0]);
|
||||||
|
for (int i = 1; i < param_count; ++i) {
|
||||||
|
out.Append(";", params[i]);
|
||||||
|
}
|
||||||
|
out.Append("m");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (foreground_.is_set() && foreground_ != from.foreground_) {
|
||||||
|
foreground_.AppendEscape(out, mode, ColorTarget::Foreground);
|
||||||
|
}
|
||||||
|
if (background_.is_set() && background_ != from.background_) {
|
||||||
|
background_.AppendEscape(out, mode, ColorTarget::Background);
|
||||||
|
}
|
||||||
|
if (underline_color_.is_set() && underline_color_ != from.underline_color_) {
|
||||||
|
underline_color_.AppendEscape(out, mode, ColorTarget::Underline);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Style::AppendColorTransitionTo(OutputBufferRef out, const Style& target,
|
||||||
|
ColorMode mode) const -> void {
|
||||||
|
CARBON_CHECK(mode != ColorMode::NoColor,
|
||||||
|
"Color transitions are only reached when color is in use.");
|
||||||
|
if (*this == target) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target.NeedsResetFrom(*this)) {
|
||||||
|
out.Append(ResetEscape);
|
||||||
|
target.AppendDiff(out, mode, Style());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
target.AppendDiff(out, mode, *this);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the name of `shape`, for printing a style.
|
||||||
|
static auto UnderlineShapeName(UnderlineShape shape) -> llvm::StringRef {
|
||||||
|
switch (shape) {
|
||||||
|
case UnderlineShape::None:
|
||||||
|
return "None";
|
||||||
|
case UnderlineShape::Single:
|
||||||
|
return "Single";
|
||||||
|
case UnderlineShape::Double:
|
||||||
|
return "Double";
|
||||||
|
case UnderlineShape::Curly:
|
||||||
|
return "Curly";
|
||||||
|
case UnderlineShape::Dotted:
|
||||||
|
return "Dotted";
|
||||||
|
case UnderlineShape::Dashed:
|
||||||
|
return "Dashed";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Style::Print(llvm::raw_ostream& out) const -> void {
|
||||||
|
out << "Style(";
|
||||||
|
llvm::ListSeparator sep;
|
||||||
|
if (bold_) {
|
||||||
|
out << sep << "bold";
|
||||||
|
}
|
||||||
|
if (dim_) {
|
||||||
|
out << sep << "dim";
|
||||||
|
}
|
||||||
|
if (italic_) {
|
||||||
|
out << sep << "italic";
|
||||||
|
}
|
||||||
|
if (reverse_) {
|
||||||
|
out << sep << "reverse";
|
||||||
|
}
|
||||||
|
if (strikethrough_) {
|
||||||
|
out << sep << "strikethrough";
|
||||||
|
}
|
||||||
|
if (underline()) {
|
||||||
|
out << sep << "underline=" << UnderlineShapeName(underline_shape_);
|
||||||
|
}
|
||||||
|
if (foreground_.is_set()) {
|
||||||
|
out << sep << "foreground=" << foreground_;
|
||||||
|
}
|
||||||
|
if (background_.is_set()) {
|
||||||
|
out << sep << "background=" << background_;
|
||||||
|
}
|
||||||
|
if (underline_color_.is_set()) {
|
||||||
|
out << sep << "underline_color=" << underline_color_;
|
||||||
|
}
|
||||||
|
out << ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Carbon::Terminal
|
||||||
@@ -0,0 +1,249 @@
|
|||||||
|
// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
|
||||||
|
// Exceptions. See /LICENSE for license information.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
||||||
|
|
||||||
|
#ifndef CARBON_COMMON_TERMINAL_STYLE_H_
|
||||||
|
#define CARBON_COMMON_TERMINAL_STYLE_H_
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstring>
|
||||||
|
#include <type_traits>
|
||||||
|
|
||||||
|
#include "common/ostream.h"
|
||||||
|
#include "common/terminal/color.h"
|
||||||
|
#include "common/terminal/output_buffer_ref.h"
|
||||||
|
#include "llvm/ADT/SmallString.h"
|
||||||
|
#include "llvm/ADT/StringRef.h"
|
||||||
|
|
||||||
|
namespace Carbon::Terminal {
|
||||||
|
|
||||||
|
// The shapes an underline can take.
|
||||||
|
//
|
||||||
|
// Only `Single` is universally understood. The rest are selected with
|
||||||
|
// colon-separated subparameters of the Select Graphic Rendition (SGR)
|
||||||
|
// underline code, which terminals limited to 16 colors mishandle, so in that
|
||||||
|
// mode they degrade to `Single` rather than disappearing.
|
||||||
|
enum class UnderlineShape : int8_t {
|
||||||
|
None,
|
||||||
|
Single,
|
||||||
|
Double,
|
||||||
|
Curly,
|
||||||
|
Dotted,
|
||||||
|
Dashed,
|
||||||
|
};
|
||||||
|
|
||||||
|
// A set of colors and text attributes to render with.
|
||||||
|
//
|
||||||
|
// Styles are values, and are composed by chaining, which keeps a named style
|
||||||
|
// readable at its definition:
|
||||||
|
//
|
||||||
|
// ```cpp
|
||||||
|
// const Style Error = Style().Bold().Foreground(AnsiColor::BrightRed);
|
||||||
|
// const Style ErrorSquiggle = Error.Underline(UnderlineShape::Curly);
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// A style authored for a rich terminal stays meaningful on a poor one. Colors
|
||||||
|
// the active `ColorMode` can't express are downsampled, an underline shape it
|
||||||
|
// can't express becomes a plain underline, and an underline color it can't
|
||||||
|
// express is left to the terminal. `NoColor` drops everything.
|
||||||
|
class Style : public Printable<Style> {
|
||||||
|
public:
|
||||||
|
// A default-constructed style sets nothing, and is both the style a terminal
|
||||||
|
// starts in and the one it is returned to.
|
||||||
|
constexpr Style() = default;
|
||||||
|
|
||||||
|
// Returns this style with the foreground color set, where an unset color
|
||||||
|
// leaves the foreground to the terminal.
|
||||||
|
auto Foreground(Color color) const -> Style {
|
||||||
|
Style result = *this;
|
||||||
|
result.foreground_ = color;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns this style with the background color set, where an unset color
|
||||||
|
// leaves the background to the terminal.
|
||||||
|
auto Background(Color color) const -> Style {
|
||||||
|
Style result = *this;
|
||||||
|
result.background_ = color;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns this style with the bold attribute set to `value`.
|
||||||
|
auto Bold(bool value = true) const -> Style {
|
||||||
|
Style result = *this;
|
||||||
|
result.bold_ = value;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns this style with the dim (faint) attribute set to `value`.
|
||||||
|
auto Dim(bool value = true) const -> Style {
|
||||||
|
Style result = *this;
|
||||||
|
result.dim_ = value;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns this style with the italic attribute set to `value`.
|
||||||
|
auto Italic(bool value = true) const -> Style {
|
||||||
|
Style result = *this;
|
||||||
|
result.italic_ = value;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns this style with reverse video set to `value`, which has the
|
||||||
|
// terminal swap foreground and background when it renders.
|
||||||
|
auto Reverse(bool value = true) const -> Style {
|
||||||
|
Style result = *this;
|
||||||
|
result.reverse_ = value;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns this style with the strikethrough attribute set to `value`.
|
||||||
|
auto Strikethrough(bool value = true) const -> Style {
|
||||||
|
Style result = *this;
|
||||||
|
result.strikethrough_ = value;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns this style with the given underline shape, where
|
||||||
|
// `UnderlineShape::None` removes one.
|
||||||
|
auto Underline(UnderlineShape shape = UnderlineShape::Single) const -> Style {
|
||||||
|
Style result = *this;
|
||||||
|
result.underline_shape_ = shape;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns this style with the underline drawn in its own color.
|
||||||
|
//
|
||||||
|
// Only `Ansi256` and richer modes can express this; elsewhere the underline
|
||||||
|
// is drawn in the foreground color, as it is when the color is unset.
|
||||||
|
auto UnderlineColor(Color color) const -> Style {
|
||||||
|
Style result = *this;
|
||||||
|
result.underline_color_ = color;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto foreground() const -> Color { return foreground_; }
|
||||||
|
auto background() const -> Color { return background_; }
|
||||||
|
auto underline_color() const -> Color { return underline_color_; }
|
||||||
|
auto underline_shape() const -> UnderlineShape { return underline_shape_; }
|
||||||
|
auto underline() const -> bool {
|
||||||
|
return underline_shape_ != UnderlineShape::None;
|
||||||
|
}
|
||||||
|
auto bold() const -> bool { return bold_; }
|
||||||
|
auto dim() const -> bool { return dim_; }
|
||||||
|
auto italic() const -> bool { return italic_; }
|
||||||
|
auto reverse() const -> bool { return reverse_; }
|
||||||
|
auto strikethrough() const -> bool { return strikethrough_; }
|
||||||
|
|
||||||
|
// Returns whether this style paints anything where there is no glyph.
|
||||||
|
//
|
||||||
|
// Attributes that only affect a glyph's own pixels, such as the foreground
|
||||||
|
// color or weight, are invisible on a blank cell. Rendering uses this to drop
|
||||||
|
// trailing blanks, and to decide when a style must be turned off before a
|
||||||
|
// newline.
|
||||||
|
auto IsVisibleOnBlank() const -> bool {
|
||||||
|
return background_.is_set() || reverse_ || strikethrough_ || underline();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Appends the escape sequences that move the terminal from this style to
|
||||||
|
// `target`.
|
||||||
|
//
|
||||||
|
// This is the only way styles reach a terminal, because turning a style on
|
||||||
|
// and turning it back off are both transitions: from and to the default
|
||||||
|
// style respectively.
|
||||||
|
//
|
||||||
|
// SGR adds attributes one at a time, but its codes for removing them are
|
||||||
|
// entangled and unevenly supported: one code clears both bold and dim, and
|
||||||
|
// the code some terminals use to clear bold is double-underline in ECMA-48.
|
||||||
|
// Dropping anything therefore costs a full reset and a fresh start, so a run
|
||||||
|
// is cheapest when every style in it sets the same attributes and the same
|
||||||
|
// colors, differing only in the color values.
|
||||||
|
auto AppendTransitionTo(OutputBufferRef out, const Style& target,
|
||||||
|
ColorMode mode) const -> void {
|
||||||
|
// Rendering calls this for every cell it emits, and with color off no
|
||||||
|
// transition can produce anything, so that case is settled here rather than
|
||||||
|
// across a call.
|
||||||
|
if (mode != ColorMode::NoColor) {
|
||||||
|
AppendColorTransitionTo(out, target, mode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Print(llvm::raw_ostream& out) const -> void;
|
||||||
|
|
||||||
|
// Rendering compares the style of every cell against the one in use, so this
|
||||||
|
// compares the bytes rather than member by member. The assertion is what
|
||||||
|
// makes that valid: every bit of a style belongs to a member, as the members
|
||||||
|
// are all byte-aligned and always initialized, so none of the bytes are
|
||||||
|
// padding.
|
||||||
|
//
|
||||||
|
// Written out rather than defaulted because the `Printable` base has no
|
||||||
|
// comparison of its own, which would leave a defaulted one deleted.
|
||||||
|
friend auto operator==(const Style& lhs, const Style& rhs) -> bool {
|
||||||
|
static_assert(std::has_unique_object_representations_v<Style>);
|
||||||
|
return std::memcmp(&lhs, &rhs, sizeof(Style)) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Turns every attribute and color off.
|
||||||
|
static constexpr llvm::StringRef ResetEscape = "\x1b[0m";
|
||||||
|
|
||||||
|
// Appends the transition to `target`, where `mode` has color.
|
||||||
|
auto AppendColorTransitionTo(OutputBufferRef out, const Style& target,
|
||||||
|
ColorMode mode) const -> void;
|
||||||
|
|
||||||
|
// Returns whether `from` sets anything this style leaves unset, which is
|
||||||
|
// exactly when the transition needs a reset.
|
||||||
|
auto NeedsResetFrom(const Style& from) const -> bool;
|
||||||
|
|
||||||
|
// Appends the escapes taking `from` to this style, which requires that this
|
||||||
|
// style drops nothing `from` set: `!NeedsResetFrom(from)`.
|
||||||
|
auto AppendDiff(OutputBufferRef out, ColorMode mode, const Style& from) const
|
||||||
|
-> void;
|
||||||
|
|
||||||
|
Color foreground_;
|
||||||
|
Color background_;
|
||||||
|
Color underline_color_;
|
||||||
|
UnderlineShape underline_shape_ = UnderlineShape::None;
|
||||||
|
bool bold_ = false;
|
||||||
|
bool dim_ = false;
|
||||||
|
bool italic_ = false;
|
||||||
|
bool reverse_ = false;
|
||||||
|
bool strikethrough_ = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Streams `text` with `style` applied and then removed again:
|
||||||
|
//
|
||||||
|
// ```cpp
|
||||||
|
// out << Styled("error", ErrorStyle, mode) << ": " << message << "\n";
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// This is the whole API for output that is a stream of styled runs. Reach for
|
||||||
|
// `Buffer` instead when output needs to be positioned in two dimensions.
|
||||||
|
//
|
||||||
|
// The text is referenced, not copied, so it must outlive the printing.
|
||||||
|
class Styled : public Printable<Styled> {
|
||||||
|
public:
|
||||||
|
Styled(llvm::StringRef text, const Style& style, ColorMode mode)
|
||||||
|
: text_(text), style_(style), mode_(mode) {}
|
||||||
|
|
||||||
|
auto Print(llvm::raw_ostream& out) const -> void {
|
||||||
|
// The escapes and the text they wrap reach the stream as one write, both
|
||||||
|
// because that is cheaper and because it keeps a styled run from being
|
||||||
|
// split across writes that something else could interleave with.
|
||||||
|
llvm::SmallString<128> storage;
|
||||||
|
OutputBufferRef bytes = storage;
|
||||||
|
Style().AppendTransitionTo(bytes, style_, mode_);
|
||||||
|
bytes.Append(text_);
|
||||||
|
style_.AppendTransitionTo(bytes, Style(), mode_);
|
||||||
|
out << storage;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
llvm::StringRef text_;
|
||||||
|
Style style_;
|
||||||
|
ColorMode mode_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Carbon::Terminal
|
||||||
|
|
||||||
|
#endif // CARBON_COMMON_TERMINAL_STYLE_H_
|
||||||
@@ -0,0 +1,305 @@
|
|||||||
|
// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
|
||||||
|
// Exceptions. See /LICENSE for license information.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
||||||
|
|
||||||
|
#include "common/terminal/style.h"
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "common/raw_string_ostream.h"
|
||||||
|
#include "llvm/ADT/STLExtras.h"
|
||||||
|
#include "llvm/Support/FormatVariadic.h"
|
||||||
|
|
||||||
|
namespace Carbon::Terminal {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
auto Transition(const Style& from, const Style& to, ColorMode mode)
|
||||||
|
-> std::string {
|
||||||
|
llvm::SmallString<64> out;
|
||||||
|
from.AppendTransitionTo(out, to, mode);
|
||||||
|
return std::string(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Turning a style on is the transition from the default style, and turning it
|
||||||
|
// off is the transition back to it.
|
||||||
|
auto Escapes(const Style& style, ColorMode mode) -> std::string {
|
||||||
|
return Transition(Style(), style, mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Reset(const Style& style, ColorMode mode) -> std::string {
|
||||||
|
return Transition(style, Style(), mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(StyleTest, Attributes) {
|
||||||
|
EXPECT_EQ(Escapes(Style().Bold(), ColorMode::Truecolor), "\x1b[1m");
|
||||||
|
EXPECT_EQ(Escapes(Style().Dim(), ColorMode::Truecolor), "\x1b[2m");
|
||||||
|
EXPECT_EQ(Escapes(Style().Italic(), ColorMode::Truecolor), "\x1b[3m");
|
||||||
|
EXPECT_EQ(Escapes(Style().Underline(), ColorMode::Truecolor), "\x1b[4m");
|
||||||
|
EXPECT_EQ(Escapes(Style().Reverse(), ColorMode::Truecolor), "\x1b[7m");
|
||||||
|
EXPECT_EQ(Escapes(Style().Strikethrough(), ColorMode::Truecolor), "\x1b[9m");
|
||||||
|
|
||||||
|
// Attributes combine into a single sequence, in ascending code order.
|
||||||
|
EXPECT_EQ(Escapes(Style()
|
||||||
|
.Italic()
|
||||||
|
.Reverse()
|
||||||
|
.Underline(UnderlineShape::Curly)
|
||||||
|
.UnderlineColor(Color(0, 255, 0))
|
||||||
|
.Strikethrough(),
|
||||||
|
ColorMode::Truecolor),
|
||||||
|
"\x1b[3;4:3;7;9m\x1b[58;2;0;255;0m");
|
||||||
|
|
||||||
|
// Every attribute at once, which is the most parameters a diff can produce
|
||||||
|
// and the bound `AppendDiff` sizes its array for.
|
||||||
|
EXPECT_EQ(Escapes(Style()
|
||||||
|
.Bold()
|
||||||
|
.Dim()
|
||||||
|
.Italic()
|
||||||
|
.Underline(UnderlineShape::Double)
|
||||||
|
.Reverse()
|
||||||
|
.Strikethrough(),
|
||||||
|
ColorMode::Truecolor),
|
||||||
|
"\x1b[1;2;3;4:2;7;9m");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(StyleTest, AttributesAndColors) {
|
||||||
|
Style style = Style().Bold().Foreground(Color(255, 0, 0));
|
||||||
|
EXPECT_EQ(Escapes(style, ColorMode::Truecolor), "\x1b[1m\x1b[38;2;255;0;0m");
|
||||||
|
EXPECT_EQ(Reset(style, ColorMode::Truecolor), "\x1b[0m");
|
||||||
|
|
||||||
|
EXPECT_EQ(Escapes(style, ColorMode::NoColor), "");
|
||||||
|
EXPECT_EQ(Reset(style, ColorMode::NoColor), "");
|
||||||
|
|
||||||
|
// A style that set nothing has nothing to reset.
|
||||||
|
EXPECT_EQ(Reset(Style(), ColorMode::Truecolor), "");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(StyleTest, UnderlineShapesDegradeInAnsi16) {
|
||||||
|
// The shaped underlines are colon subparameters, which the terminals limited
|
||||||
|
// to 16 colors mishandle. They become plain underlines rather than
|
||||||
|
// disappearing, so they still mark what they were meant to mark.
|
||||||
|
for (UnderlineShape shape :
|
||||||
|
{UnderlineShape::Single, UnderlineShape::Double, UnderlineShape::Curly,
|
||||||
|
UnderlineShape::Dotted, UnderlineShape::Dashed}) {
|
||||||
|
EXPECT_EQ(Escapes(Style().Underline(shape), ColorMode::Ansi16), "\x1b[4m");
|
||||||
|
}
|
||||||
|
|
||||||
|
EXPECT_EQ(
|
||||||
|
Escapes(Style().Underline(UnderlineShape::Double), ColorMode::Ansi256),
|
||||||
|
"\x1b[4:2m");
|
||||||
|
EXPECT_EQ(
|
||||||
|
Escapes(Style().Underline(UnderlineShape::Dotted), ColorMode::Truecolor),
|
||||||
|
"\x1b[4:4m");
|
||||||
|
EXPECT_EQ(
|
||||||
|
Escapes(Style().Underline(UnderlineShape::Dashed), ColorMode::Truecolor),
|
||||||
|
"\x1b[4:5m");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(StyleTest, TransitionAddsWithoutReset) {
|
||||||
|
Style red_bold = Style().Bold().Foreground(Color(255, 0, 0));
|
||||||
|
Style blue_bold = Style().Bold().Foreground(Color(0, 0, 255));
|
||||||
|
|
||||||
|
// Bold carries over, so only the color has to change.
|
||||||
|
EXPECT_EQ(Transition(red_bold, blue_bold, ColorMode::Truecolor),
|
||||||
|
"\x1b[38;2;0;0;255m");
|
||||||
|
|
||||||
|
// Adding an attribute needs no reset either.
|
||||||
|
EXPECT_EQ(Transition(red_bold, red_bold.Italic(), ColorMode::Truecolor),
|
||||||
|
"\x1b[3m");
|
||||||
|
|
||||||
|
// Nor does changing the shape of an underline that is already on.
|
||||||
|
EXPECT_EQ(
|
||||||
|
Transition(Style().Underline(), Style().Underline(UnderlineShape::Curly),
|
||||||
|
ColorMode::Truecolor),
|
||||||
|
"\x1b[4:3m");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(StyleTest, TransitionResetsToDrop) {
|
||||||
|
Style red_bold = Style().Bold().Foreground(Color(255, 0, 0));
|
||||||
|
|
||||||
|
// Attributes are removed with a reset and a fresh start rather than SGR's
|
||||||
|
// entangled off-codes, so dropping bold costs both.
|
||||||
|
EXPECT_EQ(Transition(red_bold, Style().Foreground(Color(255, 0, 0)),
|
||||||
|
ColorMode::Truecolor),
|
||||||
|
"\x1b[0m\x1b[38;2;255;0;0m");
|
||||||
|
|
||||||
|
// Dropping the color is the same story.
|
||||||
|
EXPECT_EQ(Transition(red_bold, Style().Bold(), ColorMode::Truecolor),
|
||||||
|
"\x1b[0m\x1b[1m");
|
||||||
|
|
||||||
|
// Returning to no style at all is just the reset.
|
||||||
|
EXPECT_EQ(Transition(red_bold, Style(), ColorMode::Truecolor), "\x1b[0m");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(StyleTest, TransitionToSelfIsEmpty) {
|
||||||
|
Style style = Style().Bold().Italic().Foreground(AnsiColor::Red);
|
||||||
|
EXPECT_EQ(Transition(style, style, ColorMode::Truecolor), "");
|
||||||
|
EXPECT_EQ(Transition(Style(), Style(), ColorMode::Truecolor), "");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(StyleTest, NoColorWritesNothing) {
|
||||||
|
Style style = Style().Bold().Foreground(Color(1, 2, 3));
|
||||||
|
EXPECT_EQ(Escapes(style, ColorMode::NoColor), "");
|
||||||
|
EXPECT_EQ(Reset(style, ColorMode::NoColor), "");
|
||||||
|
EXPECT_EQ(Transition(style, Style().Italic(), ColorMode::NoColor), "");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(StyleTest, IsVisibleOnBlank) {
|
||||||
|
// Attributes that only affect a glyph's own pixels leave a blank cell blank.
|
||||||
|
EXPECT_FALSE(Style().IsVisibleOnBlank());
|
||||||
|
EXPECT_FALSE(Style().Bold().IsVisibleOnBlank());
|
||||||
|
EXPECT_FALSE(Style().Dim().Italic().IsVisibleOnBlank());
|
||||||
|
EXPECT_FALSE(Style().Foreground(AnsiColor::Red).IsVisibleOnBlank());
|
||||||
|
|
||||||
|
// These paint the cell itself.
|
||||||
|
EXPECT_TRUE(Style().Background(AnsiColor::Red).IsVisibleOnBlank());
|
||||||
|
EXPECT_TRUE(Style().Reverse().IsVisibleOnBlank());
|
||||||
|
EXPECT_TRUE(Style().Underline().IsVisibleOnBlank());
|
||||||
|
EXPECT_TRUE(Style().Strikethrough().IsVisibleOnBlank());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(StyleTest, EqualityComparesEveryField) {
|
||||||
|
// The comparison is over the bytes of a style rather than its fields, which
|
||||||
|
// is only right as long as every byte belongs to a field.
|
||||||
|
Style base = Style();
|
||||||
|
EXPECT_EQ(base, Style());
|
||||||
|
EXPECT_NE(base, base.Bold());
|
||||||
|
EXPECT_NE(base, base.Dim());
|
||||||
|
EXPECT_NE(base, base.Italic());
|
||||||
|
EXPECT_NE(base, base.Reverse());
|
||||||
|
EXPECT_NE(base, base.Strikethrough());
|
||||||
|
EXPECT_NE(base, base.Underline());
|
||||||
|
EXPECT_NE(base, base.Foreground(AnsiColor::Red));
|
||||||
|
EXPECT_NE(base, base.Background(AnsiColor::Red));
|
||||||
|
EXPECT_NE(base, base.UnderlineColor(AnsiColor::Red));
|
||||||
|
|
||||||
|
// Including fields that differ only in value.
|
||||||
|
EXPECT_NE(Style().Foreground(AnsiColor::Red),
|
||||||
|
Style().Foreground(AnsiColor::Blue));
|
||||||
|
EXPECT_NE(Style().Underline(UnderlineShape::Curly),
|
||||||
|
Style().Underline(UnderlineShape::Dotted));
|
||||||
|
|
||||||
|
// And colors that a byte comparison could confuse, as an unset color and
|
||||||
|
// these two both hold nothing but zeroes in their channels.
|
||||||
|
EXPECT_NE(base, base.Foreground(AnsiColor::Black));
|
||||||
|
EXPECT_NE(base, base.Foreground(Color(0, 0, 0)));
|
||||||
|
EXPECT_NE(base.Foreground(AnsiColor::Black), base.Foreground(Color(0, 0, 0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(StyleTest, ColorsCanBeCleared) {
|
||||||
|
Style red = Style().Foreground(AnsiColor::Red);
|
||||||
|
EXPECT_TRUE(red.foreground().is_set());
|
||||||
|
|
||||||
|
Style cleared = red.Foreground(Color());
|
||||||
|
EXPECT_FALSE(cleared.foreground().is_set());
|
||||||
|
EXPECT_EQ(cleared, Style());
|
||||||
|
|
||||||
|
// Dropping a color is a reset like dropping an attribute.
|
||||||
|
llvm::SmallString<64> bytes;
|
||||||
|
red.AppendTransitionTo(bytes, cleared, ColorMode::Ansi16);
|
||||||
|
EXPECT_EQ(bytes, "\x1b[0m");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(StyleTest, Ansi16Transitions) {
|
||||||
|
// Every shape renders as a plain underline here, so switching between two of
|
||||||
|
// them is not a change the terminal can see.
|
||||||
|
EXPECT_EQ(
|
||||||
|
Transition(Style().Underline(), Style().Underline(UnderlineShape::Curly),
|
||||||
|
ColorMode::Ansi16),
|
||||||
|
"");
|
||||||
|
|
||||||
|
// The 16 colors still transition normally.
|
||||||
|
EXPECT_EQ(Transition(Style().Foreground(AnsiColor::Red),
|
||||||
|
Style().Foreground(AnsiColor::Blue), ColorMode::Ansi16),
|
||||||
|
"\x1b[34m");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(StyleTest, ChainingLeavesTheOriginalAlone) {
|
||||||
|
const Style base = Style().Bold();
|
||||||
|
const Style derived = base.Foreground(AnsiColor::Red);
|
||||||
|
|
||||||
|
EXPECT_EQ(base, Style().Bold());
|
||||||
|
EXPECT_EQ(derived, Style().Bold().Foreground(AnsiColor::Red));
|
||||||
|
EXPECT_NE(base, derived);
|
||||||
|
|
||||||
|
EXPECT_EQ(Style().Bold().Bold(false), Style());
|
||||||
|
EXPECT_EQ(Style().Underline().Underline(UnderlineShape::None), Style());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(StyleTest, StyledStreaming) {
|
||||||
|
RawStringOstream out;
|
||||||
|
out << Styled("error", Style().Bold().Foreground(AnsiColor::BrightRed),
|
||||||
|
ColorMode::Ansi16)
|
||||||
|
<< ": bad";
|
||||||
|
EXPECT_EQ(out.TakeStr(), "\x1b[1m\x1b[91merror\x1b[0m: bad");
|
||||||
|
|
||||||
|
out << Styled("error", Style().Bold(), ColorMode::NoColor) << ": bad";
|
||||||
|
EXPECT_EQ(out.TakeStr(), "error: bad");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(StyleTest, Print) {
|
||||||
|
EXPECT_EQ(PrintToString(Style()), "Style()");
|
||||||
|
EXPECT_EQ(PrintToString(Style().Bold().Foreground(AnsiColor::Red)),
|
||||||
|
"Style(bold, foreground=Red)");
|
||||||
|
EXPECT_EQ(PrintToString(Style()
|
||||||
|
.Dim()
|
||||||
|
.Italic()
|
||||||
|
.Underline(UnderlineShape::Curly)
|
||||||
|
.UnderlineColor(Color(0, 0, 1))
|
||||||
|
.Background(Color(1, 2, 3))),
|
||||||
|
"Style(dim, italic, underline=Curly, background=#010203, "
|
||||||
|
"underline_color=#000001)");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(StyleTest, EveryPairOfStylesHasATransition) {
|
||||||
|
// Every pair of styles has to have a transition, including the ones that
|
||||||
|
// drop an attribute and so need a reset rather than a diff.
|
||||||
|
std::vector<Style> styles = {
|
||||||
|
Style(),
|
||||||
|
Style().Bold(),
|
||||||
|
Style().Dim().Italic(),
|
||||||
|
Style().Reverse().Strikethrough(),
|
||||||
|
Style().Underline(UnderlineShape::Curly),
|
||||||
|
Style().Underline(UnderlineShape::Double).UnderlineColor(AnsiColor::Red),
|
||||||
|
Style().Foreground(AnsiColor::BrightRed).Background(AnsiColor::Black),
|
||||||
|
Style().Foreground(Color(1, 2, 3)).Background(Color(250, 251, 252)),
|
||||||
|
Style()
|
||||||
|
.Bold()
|
||||||
|
.Dim()
|
||||||
|
.Italic()
|
||||||
|
.Reverse()
|
||||||
|
.Strikethrough()
|
||||||
|
.Underline(UnderlineShape::Dashed)
|
||||||
|
.Foreground(Color(9, 9, 9))
|
||||||
|
.Background(AnsiColor::White)
|
||||||
|
.UnderlineColor(Color(4, 5, 6)),
|
||||||
|
};
|
||||||
|
for (ColorMode mode : {ColorMode::NoColor, ColorMode::Ansi16,
|
||||||
|
ColorMode::Ansi256, ColorMode::Truecolor}) {
|
||||||
|
for (auto [from_index, from] : llvm::enumerate(styles)) {
|
||||||
|
for (auto [to_index, to] : llvm::enumerate(styles)) {
|
||||||
|
std::string out = Transition(from, to, mode);
|
||||||
|
std::string pair =
|
||||||
|
llvm::formatv("{0} -> {1}", from_index, to_index).str();
|
||||||
|
if (mode == ColorMode::NoColor) {
|
||||||
|
// Nothing is said at all when nothing can be shown.
|
||||||
|
EXPECT_TRUE(out.empty()) << pair;
|
||||||
|
} else if (from == to) {
|
||||||
|
// Staying where it already is costs nothing, whatever the mode.
|
||||||
|
EXPECT_TRUE(out.empty()) << pair;
|
||||||
|
} else if (mode == ColorMode::Truecolor) {
|
||||||
|
// Truecolor is the one mode that can express every field, so no two
|
||||||
|
// distinct styles render the same and every step has to say
|
||||||
|
// something. The narrower modes round colors together, so there two
|
||||||
|
// styles can genuinely be one and the transition is empty.
|
||||||
|
EXPECT_FALSE(out.empty()) << pair;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
} // namespace Carbon::Terminal
|
||||||
@@ -0,0 +1,247 @@
|
|||||||
|
// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
|
||||||
|
// Exceptions. See /LICENSE for license information.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
||||||
|
|
||||||
|
#include <benchmark/benchmark.h>
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
|
||||||
|
#include "absl/random/random.h"
|
||||||
|
#include "common/terminal/buffer.h"
|
||||||
|
#include "common/terminal/capabilities.h"
|
||||||
|
#include "common/terminal/color.h"
|
||||||
|
#include "common/terminal/style.h"
|
||||||
|
#include "llvm/ADT/SmallString.h"
|
||||||
|
#include "llvm/ADT/SmallVector.h"
|
||||||
|
|
||||||
|
namespace Carbon::Terminal {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
static auto RandomColor(absl::BitGen& bitgen) -> Color {
|
||||||
|
return {absl::Uniform<uint8_t>(bitgen), absl::Uniform<uint8_t>(bitgen),
|
||||||
|
absl::Uniform<uint8_t>(bitgen)};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Benchmarks style transitions with a store-to-load dependency on the rendered
|
||||||
|
// buffer, so each iteration waits on the one before it.
|
||||||
|
static void BM_StyleTransition(benchmark::State& state, ColorMode mode) {
|
||||||
|
// A large pool of styles keeps branch prediction from learning the innards.
|
||||||
|
constexpr int PoolSize = 1024;
|
||||||
|
std::array<Style, PoolSize> styles;
|
||||||
|
|
||||||
|
absl::BitGen bitgen;
|
||||||
|
|
||||||
|
// Generate a pool of styles. All styles have the same set of attributes
|
||||||
|
// enabled (bold, italic, foreground color, background color, underline
|
||||||
|
// color, and underline style), but with different random RGB values. This is
|
||||||
|
// the case where no reset is needed and only colors change.
|
||||||
|
for (int i = 0; i < PoolSize; ++i) {
|
||||||
|
styles[i] = Style()
|
||||||
|
.Bold()
|
||||||
|
.Italic()
|
||||||
|
.Foreground(RandomColor(bitgen))
|
||||||
|
.Background(RandomColor(bitgen))
|
||||||
|
.Underline(UnderlineShape::Curly)
|
||||||
|
.UnderlineColor(RandomColor(bitgen));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The style transitions accumulate in a reused buffer, for a small but
|
||||||
|
// stable per-iteration overhead.
|
||||||
|
llvm::SmallString<1024> str;
|
||||||
|
|
||||||
|
int current_idx = 0;
|
||||||
|
for (auto _ : state) {
|
||||||
|
int next_idx = (current_idx + 1) % PoolSize;
|
||||||
|
styles[current_idx].AppendTransitionTo(str, styles[next_idx], mode);
|
||||||
|
// Reading the string's terminator makes each iteration wait on the store
|
||||||
|
// the one before it made, and blocks the optimizer from guessing the
|
||||||
|
// value.
|
||||||
|
uint8_t last_byte = str.c_str()[str.size()];
|
||||||
|
benchmark::DoNotOptimize(last_byte);
|
||||||
|
current_idx = (next_idx + last_byte) % PoolSize;
|
||||||
|
str.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BENCHMARK_CAPTURE(BM_StyleTransition, NoColor, ColorMode::NoColor);
|
||||||
|
BENCHMARK_CAPTURE(BM_StyleTransition, Ansi16, ColorMode::Ansi16);
|
||||||
|
BENCHMARK_CAPTURE(BM_StyleTransition, Ansi256, ColorMode::Ansi256);
|
||||||
|
BENCHMARK_CAPTURE(BM_StyleTransition, Truecolor, ColorMode::Truecolor);
|
||||||
|
|
||||||
|
// Benchmarks `Buffer::Render` for terminal-sized screens using a
|
||||||
|
// data-dependency feedback loop where the next buffer index depends on the
|
||||||
|
// bytes written in the previous iteration.
|
||||||
|
static void BM_BufferRender(benchmark::State& state, ColorMode mode) {
|
||||||
|
const int width = state.range(0);
|
||||||
|
const int height = state.range(1);
|
||||||
|
|
||||||
|
// Given the significantly larger body of work, a much smaller pool suffices
|
||||||
|
// without branch prediction skewing results.
|
||||||
|
constexpr int PoolSize = 16;
|
||||||
|
llvm::SmallVector<Buffer, PoolSize> buffers;
|
||||||
|
|
||||||
|
absl::BitGen bitgen;
|
||||||
|
|
||||||
|
// Generate a pool of buffers. To ensure workload consistency but without
|
||||||
|
// being identical, cell (x, y) in all buffers have:
|
||||||
|
// - The same style attributes enabled (fg, bg, bold, italic etc.).
|
||||||
|
// - Different random color and character values.
|
||||||
|
// - A different shape (a box of varying aspect ratio starting at (1, 1) with
|
||||||
|
// constant perimeter of 60 cells).
|
||||||
|
for (int i = 0; i < PoolSize; ++i) {
|
||||||
|
Buffer buffer(width, Charset::Utf8);
|
||||||
|
|
||||||
|
auto get_style = [&](int x, int y) {
|
||||||
|
if ((x + y) % 3 == 0) {
|
||||||
|
return Style().Foreground(RandomColor(bitgen)).Bold();
|
||||||
|
}
|
||||||
|
if ((x + y) % 3 == 1) {
|
||||||
|
return Style().Background(RandomColor(bitgen)).Italic();
|
||||||
|
}
|
||||||
|
return Style()
|
||||||
|
.Underline(UnderlineShape::Single)
|
||||||
|
.UnderlineColor(RandomColor(bitgen));
|
||||||
|
};
|
||||||
|
|
||||||
|
for (int y = 0; y < height; ++y) {
|
||||||
|
for (int x = 0; x < width; ++x) {
|
||||||
|
buffer.DrawCodePoint(x, y, U'A' + absl::Uniform(bitgen, 0, 26),
|
||||||
|
get_style(x, y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int box_height = 6 + i;
|
||||||
|
buffer.DrawBox(1, 1, 32 - box_height, box_height, get_style(1, 1));
|
||||||
|
|
||||||
|
buffers.push_back(std::move(buffer));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The rendered output accumulates in a reused buffer, for a small but
|
||||||
|
// stable per-iteration overhead.
|
||||||
|
llvm::SmallString<1 << 16> str;
|
||||||
|
|
||||||
|
int current_idx = 0;
|
||||||
|
for (auto _ : state) {
|
||||||
|
buffers[current_idx].Render(str, mode);
|
||||||
|
// Reading the string's terminator makes each iteration wait on the store
|
||||||
|
// the one before it made, and blocks the optimizer from guessing the
|
||||||
|
// value.
|
||||||
|
uint8_t last_byte = str.c_str()[str.size()];
|
||||||
|
benchmark::DoNotOptimize(last_byte);
|
||||||
|
current_idx = (current_idx + 1 + last_byte) % PoolSize;
|
||||||
|
str.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BENCHMARK_CAPTURE(BM_BufferRender, NoColor, ColorMode::NoColor)
|
||||||
|
->Args({80, 24})
|
||||||
|
->Args({120, 40});
|
||||||
|
BENCHMARK_CAPTURE(BM_BufferRender, Ansi16, ColorMode::Ansi16)
|
||||||
|
->Args({80, 24})
|
||||||
|
->Args({120, 40});
|
||||||
|
BENCHMARK_CAPTURE(BM_BufferRender, Ansi256, ColorMode::Ansi256)
|
||||||
|
->Args({80, 24})
|
||||||
|
->Args({120, 40});
|
||||||
|
BENCHMARK_CAPTURE(BM_BufferRender, Truecolor, ColorMode::Truecolor)
|
||||||
|
->Args({80, 24})
|
||||||
|
->Args({120, 40});
|
||||||
|
|
||||||
|
// A line of source of the sort a diagnostic quotes, in the two forms that
|
||||||
|
// matter for column measurement. The second has a double-width character and a
|
||||||
|
// combining mark, spelled out because the precomposed form is a single code
|
||||||
|
// point and wouldn't exercise marks at all.
|
||||||
|
static constexpr llvm::StringLiteral AsciiSource =
|
||||||
|
"auto Foo(i32 x) -> i32 { return x * 42; }";
|
||||||
|
static constexpr llvm::StringLiteral UnicodeSource =
|
||||||
|
"var 中文: String = \"he\xcc\x81llo\";";
|
||||||
|
|
||||||
|
// Benchmarks drawing text, which is where column measurement is paid. The
|
||||||
|
// three cases cover the regimes it runs in: no UTF-8 processing at all, UTF-8
|
||||||
|
// processing over text that turns out to be ASCII, and UTF-8 processing over
|
||||||
|
// text that isn't.
|
||||||
|
static void BM_DrawText(benchmark::State& state, Charset charset,
|
||||||
|
llvm::StringRef text) {
|
||||||
|
constexpr int Width = 120;
|
||||||
|
Buffer buffer(Width, charset);
|
||||||
|
|
||||||
|
int row = 0;
|
||||||
|
for (auto _ : state) {
|
||||||
|
row = buffer.DrawText(0, row, text, Style()).y + 1;
|
||||||
|
benchmark::DoNotOptimize(row);
|
||||||
|
// Reuse a bounded band of rows so this measures drawing rather than the
|
||||||
|
// buffer's growth.
|
||||||
|
if (row > 64) {
|
||||||
|
row = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BENCHMARK_CAPTURE(BM_DrawText, AsciiCharset, Charset::Ascii, AsciiSource);
|
||||||
|
BENCHMARK_CAPTURE(BM_DrawText, Utf8Charset, Charset::Utf8, AsciiSource);
|
||||||
|
BENCHMARK_CAPTURE(BM_DrawText, Utf8CharsetWithUnicode, Charset::Utf8,
|
||||||
|
UnicodeSource);
|
||||||
|
|
||||||
|
// The size of the boxes the line art benchmarks draw, chosen so that one is
|
||||||
|
// about as large as the frame around a quoted snippet.
|
||||||
|
constexpr int BoxWidth = 40;
|
||||||
|
constexpr int BoxHeight = 12;
|
||||||
|
|
||||||
|
// Benchmarks drawing line art, which is where junction bookkeeping is paid.
|
||||||
|
// Every cell of a box is a separate glyph decision, and boxes are drawn over
|
||||||
|
// whatever was there before, so this covers clearing as well as drawing.
|
||||||
|
static void BM_DrawBox(benchmark::State& state, Charset charset) {
|
||||||
|
constexpr int Width = 120;
|
||||||
|
constexpr int Rows = 64;
|
||||||
|
Buffer buffer(Width, charset);
|
||||||
|
Style style = Style().Foreground(AnsiColor::Blue);
|
||||||
|
|
||||||
|
// Boxes are drawn over a band of rows wider than one box, so that they land
|
||||||
|
// on a mix of blank cells and cells already holding line art.
|
||||||
|
int y = 0;
|
||||||
|
for (auto _ : state) {
|
||||||
|
buffer.DrawBox(0, y, BoxWidth, BoxHeight, style);
|
||||||
|
y = (y + BoxHeight) % (Rows - BoxHeight);
|
||||||
|
benchmark::DoNotOptimize(y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BENCHMARK_CAPTURE(BM_DrawBox, AsciiCharset, Charset::Ascii);
|
||||||
|
BENCHMARK_CAPTURE(BM_DrawBox, Utf8Charset, Charset::Utf8);
|
||||||
|
|
||||||
|
// Benchmarks rendering line art, which is the non-ASCII rendering that comes
|
||||||
|
// up in practice: the text a diagnostic quotes is nearly always ASCII, while
|
||||||
|
// the frames and connectors around it are box-drawing characters that each
|
||||||
|
// encode to three bytes.
|
||||||
|
static void BM_RenderLineArt(benchmark::State& state, ColorMode mode) {
|
||||||
|
constexpr int Width = 120;
|
||||||
|
constexpr int PoolSize = 16;
|
||||||
|
llvm::SmallVector<Buffer, PoolSize> buffers;
|
||||||
|
|
||||||
|
absl::BitGen bitgen;
|
||||||
|
|
||||||
|
for (int i = 0; i < PoolSize; ++i) {
|
||||||
|
Buffer buffer(Width, Charset::Utf8);
|
||||||
|
// Overlapping boxes offset by a row and two columns each, so the rendered
|
||||||
|
// rows carry line art and junctions wherever the edges cross.
|
||||||
|
for (int box = 0; box < 4; ++box) {
|
||||||
|
buffer.DrawBox(box * 2, box, BoxWidth + 2 * box, BoxHeight,
|
||||||
|
Style().Foreground(RandomColor(bitgen)));
|
||||||
|
}
|
||||||
|
buffers.push_back(std::move(buffer));
|
||||||
|
}
|
||||||
|
|
||||||
|
llvm::SmallString<1 << 14> str;
|
||||||
|
|
||||||
|
int current_idx = 0;
|
||||||
|
for (auto _ : state) {
|
||||||
|
buffers[current_idx].Render(str, mode);
|
||||||
|
// Reading the string's terminator makes each iteration wait on the store
|
||||||
|
// the one before it made, and blocks the optimizer from guessing the
|
||||||
|
// value.
|
||||||
|
uint8_t last_byte = str.c_str()[str.size()];
|
||||||
|
benchmark::DoNotOptimize(last_byte);
|
||||||
|
current_idx = (current_idx + 1 + last_byte) % PoolSize;
|
||||||
|
str.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BENCHMARK_CAPTURE(BM_RenderLineArt, NoColor, ColorMode::NoColor);
|
||||||
|
BENCHMARK_CAPTURE(BM_RenderLineArt, Truecolor, ColorMode::Truecolor);
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
} // namespace Carbon::Terminal
|
||||||
Reference in New Issue
Block a user