mirror of
https://github.com/carbon-language/carbon-lang.git
synced 2026-09-24 22:02:23 +01:00
Everything drawn into a buffer was checked against `columns()`. That is
right for wrapping and for line drawing, both of which have somewhere
else to put what doesn't fit, but wrong for `DrawText`, which exists for
text that must not be broken and sometimes has to run past the width
with no other answer available. It and `DrawCodePoint` now check only
that the column is non-negative and the row is one a grid can index, and
widen the buffer as far as the text needs; `DrawWrappedText`,
`DrawHorizontalLine`, `DrawVerticalLine`, and `DrawBox` are unchanged.
That also settles what a caller does after a drawing overhangs, since
`DrawEnd` exists so that a run can continue where the last one ended,
and that continuation was itself a checked error whenever the previous
run overhung. A column computed to be negative, such as a gutter
narrower than the line number it holds, still fails.
A color picked to read against black is hard to read against white, and
nothing in `Capabilities` said which a stream was going into.
`ChooseBackground` reads `COLORFGBG`, which `rxvt` and its derivatives
set to the foreground and background palette indices, and takes anything
it doesn't answer to be dark: guessing dark costs contrast, while
guessing light puts pale text on a pale background. Asking the terminal
itself with an `OSC 11` query is the accurate answer, and needs raw
mode, a timeout, and somewhere to put the reply, so there is a TODO for
it rather than an implementation.
Every corner, tee, and crossing came out of `Charset::Ascii` as `+`,
which left six of the shapes a diagnostic draws indistinguishable: the
rule closing a frame read as the one separating two snippets, and the
anchor opening a diagnostic as the one carrying it on. Each stand-in now
keeps the axis its line runs through, which leaves `+` meaning a
crossing and nothing else. A tee keeps its through-stroke and leaves the
branch to what is drawn beside it, and a corner is `.` where its line
leaves downward and `'` where it arrives from above, which is where
those characters sit in their cells. A box is a box again:
```
+--+ .--.
| | -> | |
+--+ '--'
```
Assisted-by: Claude Code
244 lines
8.6 KiB
C++
244 lines
8.6 KiB
C++
// 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;
|
|
}
|
|
|
|
auto ChooseBackground(BackgroundPreference preference,
|
|
llvm::StringRef colorfgbg) -> Background {
|
|
switch (preference) {
|
|
case BackgroundPreference::Dark:
|
|
return Background::Dark;
|
|
case BackgroundPreference::Light:
|
|
return Background::Light;
|
|
case BackgroundPreference::Auto:
|
|
break;
|
|
}
|
|
|
|
// The background is the last field, since some terminals write a third one
|
|
// between the foreground and it.
|
|
llvm::StringRef background = colorfgbg.rsplit(';').second;
|
|
unsigned index = 0;
|
|
if (!llvm::to_integer(background, index) || index > 15) {
|
|
// Anything else, including the `default` some terminals write and the
|
|
// variable being unset, says nothing.
|
|
return Background::Dark;
|
|
}
|
|
// The first eight palette entries are the dark half, except that the eighth
|
|
// is white and the ninth is the dark gray that follows it.
|
|
return (index <= 6 || index == 8) ? Background::Dark : Background::Light;
|
|
}
|
|
|
|
// Returns the locale that determines the terminal's character encoding,
|
|
// following the precedence POSIX defines for `LC_CTYPE`.
|
|
static auto GetLocale() -> llvm::StringRef {
|
|
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.background =
|
|
ChooseBackground(preferences.background, GetEnv("COLORFGBG"));
|
|
capabilities.columns = GetColumns(fd);
|
|
|
|
return capabilities;
|
|
}
|
|
|
|
} // namespace Carbon::Terminal
|