blob: e820774244e12cb116c8258f2993726e13ccb7b6 [file] [log] [blame]
// Copyright 2024 The Dawn & Tint Authors
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this
// list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
// GEN_BUILD:CONDITION(tint_build_is_linux || tint_build_is_mac)
#include <unistd.h>
#include <termios.h>
#include <chrono>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <optional>
#include <string_view>
#include <utility>
#include "src/tint/utils/containers/vector.h"
#include "src/tint/utils/macros/compiler.h"
#include "src/tint/utils/macros/defer.h"
#include "src/tint/utils/system/env.h"
#include "src/tint/utils/system/terminal.h"
namespace tint {
namespace {
std::optional<bool> TerminalIsDarkImpl(FILE* out) {
// Check the terminal can be queried, and supports colors.
if (!isatty(STDIN_FILENO) || !TerminalSupportsColors(out)) {
return std::nullopt;
}
// Get the file descriptor for 'out'
int out_fd = fileno(out);
if (out_fd == -1) {
return std::nullopt;
}
// Store the current attributes for 'out', restore it before returning
termios original_state{};
if (tcgetattr(out_fd, &original_state) != 0) {
return std::nullopt;
}
TINT_DEFER(tcsetattr(out_fd, TCSADRAIN, &original_state));
// Prevent echoing.
termios state = original_state;
state.c_lflag &= ~static_cast<tcflag_t>(ECHO | ICANON);
if (tcsetattr(out_fd, TCSADRAIN, &state) != 0) {
return std::nullopt;
}
// Emit the device control escape sequence to query the terminal colors.
static constexpr std::string_view kQuery = "\033]11;?\033\\";
fwrite(kQuery.data(), 1, kQuery.length(), out);
fflush(out);
// Timeout for attempting to read the response.
static constexpr auto kTimeout = std::chrono::milliseconds(300);
// Record the start time.
auto start = std::chrono::steady_clock::now();
// Returns true if there's data available on stdin, or false if no data was available after
// 100ms.
auto poll_stdin = [] {
// Note: These macros introduce identifiers that start with `__`.
TINT_BEGIN_DISABLE_WARNING(RESERVED_IDENTIFIER);
fd_set rfds{};
FD_ZERO(&rfds);
FD_SET(STDIN_FILENO, &rfds);
timeval tv{};
tv.tv_sec = 0;
tv.tv_usec = 100'000;
int res = select(STDIN_FILENO + 1, &rfds, nullptr, nullptr, &tv);
return res > 0 && FD_ISSET(STDIN_FILENO, &rfds);
TINT_END_DISABLE_WARNING(RESERVED_IDENTIFIER);
};
// Helpers for parsing the response.
Vector<char, 8> peek;
auto read = [&]() -> std::optional<char> {
if (!peek.IsEmpty()) {
return peek.Pop();
}
while ((std::chrono::steady_clock::now() - start) < kTimeout) {
if (!poll_stdin()) {
return std::nullopt;
}
char c;
if (fread(&c, 1, 1, stdin) == 1) {
return c;
}
}
return std::nullopt;
};
auto match = [&](std::string_view str) {
for (size_t i = 0; i < str.length(); i++) {
auto c = read();
if (c != str[i]) {
if (c) {
peek.Push(*c);
}
while (i != 0) {
peek.Push(str[--i]);
}
return false;
}
}
return true;
};
struct Hex {
uint32_t num = 0; // The parsed hex number
uint32_t len = 0; // Number of hex digits parsed
};
auto hex = [&] {
uint32_t num = 0;
uint32_t len = 0;
while (auto c = read()) {
if (c >= '0' && c <= '9') {
num = num * 16 + static_cast<uint32_t>(*c - '0');
len++;
} else if (c >= 'a' && c <= 'z') {
num = num * 16 + 10 + static_cast<uint32_t>(*c - 'a');
len++;
} else if (c >= 'A' && c <= 'Z') {
num = num * 16 + 10 + static_cast<uint32_t>(*c - 'A');
len++;
} else {
peek.Push(*c);
break;
}
}
return Hex{num, len};
};
if (!match("\033]11;rgb:")) {
return std::nullopt;
}
auto r_i = hex();
if (!match("/")) {
return std::nullopt;
}
auto g_i = hex();
if (!match("/")) {
return std::nullopt;
}
auto b_i = hex();
if (r_i.len != g_i.len || g_i.len != b_i.len) {
return std::nullopt;
}
uint32_t max = 0;
switch (r_i.len) {
case 2: // rr/gg/bb
max = 0xff;
break;
case 4: // rrrr/gggg/bbbb
max = 0xffff;
break;
default:
return std::nullopt;
}
if (!match("\x07") && !match("\x1b")) {
return std::nullopt;
}
// https://en.wikipedia.org/wiki/Relative_luminance
float r = static_cast<float>(r_i.num) / static_cast<float>(max);
float g = static_cast<float>(g_i.num) / static_cast<float>(max);
float b = static_cast<float>(b_i.num) / static_cast<float>(max);
return (0.2126f * r + 0.7152f * g + 0.0722f * b) < 0.5f;
}
} // namespace
bool TerminalSupportsColors(FILE* f) {
if (!isatty(fileno(f))) {
return false;
}
if (auto term = GetEnvVar("TERM"); !term.empty()) {
return term == "cygwin" || term == "linux" || term == "rxvt-unicode-256color" ||
term == "rxvt-unicode" || term == "screen-256color" || term == "screen" ||
term == "tmux-256color" || term == "tmux" || term == "xterm-256color" ||
term == "xterm-color" || term == "xterm";
}
return false;
}
/// Probes the terminal using a Device Control escape sequence to get the background color.
/// @see https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Device-Control-functions
std::optional<bool> TerminalIsDark(FILE* out) {
static std::optional<bool> result = TerminalIsDarkImpl(out);
return result;
}
} // namespace tint