|
| 1 | +#ifndef CLASP_COLOR_HPP |
| 2 | +#define CLASP_COLOR_HPP |
| 3 | + |
| 4 | +#include <cstdlib> |
| 5 | +#include <cstdint> |
| 6 | +#include <optional> |
| 7 | +#include <string> |
| 8 | +#include <string_view> |
| 9 | + |
| 10 | +namespace clasp { |
| 11 | + |
| 12 | +enum class ColorMode { |
| 13 | + Auto, |
| 14 | + Always, |
| 15 | + Never, |
| 16 | +}; |
| 17 | + |
| 18 | +enum class ColorThemeName { |
| 19 | + Vscode, |
| 20 | + Sublime, |
| 21 | + Iterm2, |
| 22 | +}; |
| 23 | + |
| 24 | +enum class ColorRole { |
| 25 | + Section, |
| 26 | + Command, |
| 27 | + Flag, |
| 28 | + Type, |
| 29 | + Meta, |
| 30 | + Error, |
| 31 | +}; |
| 32 | + |
| 33 | +struct ColorTheme { |
| 34 | + std::string reset{"\x1b[0m"}; |
| 35 | + std::string section; |
| 36 | + std::string command; |
| 37 | + std::string flag; |
| 38 | + std::string type; |
| 39 | + std::string meta; |
| 40 | + std::string error; |
| 41 | +}; |
| 42 | + |
| 43 | +namespace color { |
| 44 | + |
| 45 | +enum class Stream { |
| 46 | + Stdout, |
| 47 | + Stderr, |
| 48 | + Other, |
| 49 | +}; |
| 50 | + |
| 51 | +// Returns true if the underlying stream is a terminal. |
| 52 | +bool isTty(Stream stream); |
| 53 | + |
| 54 | +// Windows: enables VT sequences for the console. Non-Windows: no-op returning true. |
| 55 | +bool enableVirtualTerminalProcessing(Stream stream); |
| 56 | + |
| 57 | +inline bool envNoColor() { |
| 58 | + // https://no-color.org/ |
| 59 | + return std::getenv("NO_COLOR") != nullptr; |
| 60 | +} |
| 61 | + |
| 62 | +inline bool envTermDumb() { |
| 63 | + const char* term = std::getenv("TERM"); |
| 64 | + return term != nullptr && std::string_view(term) == "dumb"; |
| 65 | +} |
| 66 | + |
| 67 | +inline std::string ansiRgbFg(std::uint8_t r, std::uint8_t g, std::uint8_t b) { |
| 68 | + return "\x1b[38;2;" + std::to_string(r) + ";" + std::to_string(g) + ";" + std::to_string(b) + "m"; |
| 69 | +} |
| 70 | + |
| 71 | +inline std::string ansiBold() { return "\x1b[1m"; } |
| 72 | +inline std::string ansiDim() { return "\x1b[2m"; } |
| 73 | + |
| 74 | +inline const ColorTheme& builtinTheme(ColorThemeName name) { |
| 75 | + // Note: these themes intentionally use ANSI truecolor for close matches. Terminals without truecolor |
| 76 | + // will typically approximate, and this feature is opt-in via Command::enableColor(). |
| 77 | + static const ColorTheme vscode = [] { |
| 78 | + ColorTheme t; |
| 79 | + t.section = ansiBold() + ansiRgbFg(86, 156, 214); // blue |
| 80 | + t.command = ansiRgbFg(78, 201, 176); // teal |
| 81 | + t.flag = ansiRgbFg(156, 220, 254); // light blue |
| 82 | + t.type = ansiRgbFg(206, 145, 120); // orange |
| 83 | + t.meta = ansiDim() + ansiRgbFg(160, 160, 160); // dim gray |
| 84 | + t.error = ansiBold() + ansiRgbFg(244, 71, 71); // red |
| 85 | + return t; |
| 86 | + }(); |
| 87 | + static const ColorTheme sublime = [] { |
| 88 | + ColorTheme t; |
| 89 | + t.section = ansiBold() + ansiRgbFg(249, 38, 114); // pink |
| 90 | + t.command = ansiRgbFg(166, 226, 46); // green |
| 91 | + t.flag = ansiRgbFg(102, 217, 239); // cyan |
| 92 | + t.type = ansiRgbFg(253, 151, 31); // orange |
| 93 | + t.meta = ansiDim() + ansiRgbFg(160, 160, 160); // dim gray |
| 94 | + t.error = ansiBold() + ansiRgbFg(249, 38, 114); // pink |
| 95 | + return t; |
| 96 | + }(); |
| 97 | + static const ColorTheme iterm2 = [] { |
| 98 | + ColorTheme t; |
| 99 | + t.section = "\x1b[1m\x1b[36m"; // bold cyan |
| 100 | + t.command = "\x1b[32m"; // green |
| 101 | + t.flag = "\x1b[33m"; // yellow |
| 102 | + t.type = "\x1b[35m"; // magenta |
| 103 | + t.meta = "\x1b[2m"; // dim |
| 104 | + t.error = "\x1b[1m\x1b[31m"; // bold red |
| 105 | + return t; |
| 106 | + }(); |
| 107 | + switch (name) { |
| 108 | + case ColorThemeName::Vscode: return vscode; |
| 109 | + case ColorThemeName::Sublime: return sublime; |
| 110 | + case ColorThemeName::Iterm2: return iterm2; |
| 111 | + } |
| 112 | + return vscode; |
| 113 | +} |
| 114 | + |
| 115 | +inline std::optional<ColorMode> parseMode(std::string_view s) { |
| 116 | + if (s == "auto") return ColorMode::Auto; |
| 117 | + if (s == "always") return ColorMode::Always; |
| 118 | + if (s == "never") return ColorMode::Never; |
| 119 | + return std::nullopt; |
| 120 | +} |
| 121 | + |
| 122 | +inline std::string_view modeName(ColorMode m) { |
| 123 | + switch (m) { |
| 124 | + case ColorMode::Auto: return "auto"; |
| 125 | + case ColorMode::Always: return "always"; |
| 126 | + case ColorMode::Never: return "never"; |
| 127 | + } |
| 128 | + return "auto"; |
| 129 | +} |
| 130 | + |
| 131 | +inline std::optional<ColorThemeName> parseTheme(std::string_view s) { |
| 132 | + if (s == "vscode") return ColorThemeName::Vscode; |
| 133 | + if (s == "sublime") return ColorThemeName::Sublime; |
| 134 | + if (s == "iterm2") return ColorThemeName::Iterm2; |
| 135 | + return std::nullopt; |
| 136 | +} |
| 137 | + |
| 138 | +inline std::string_view themeName(ColorThemeName t) { |
| 139 | + switch (t) { |
| 140 | + case ColorThemeName::Vscode: return "vscode"; |
| 141 | + case ColorThemeName::Sublime: return "sublime"; |
| 142 | + case ColorThemeName::Iterm2: return "iterm2"; |
| 143 | + } |
| 144 | + return "vscode"; |
| 145 | +} |
| 146 | + |
| 147 | +} // namespace color |
| 148 | +} // namespace clasp |
| 149 | + |
| 150 | +#endif // CLASP_COLOR_HPP |
0 commit comments