FE 0.10.0
Header-only C++ frontend library
Loading...
Searching...
No Matches
term.h
Go to the documentation of this file.
1#pragma once
2
3#include <cstdlib>
4#include <cstring>
5
6#include <atomic>
7#include <iostream>
8#include <ostream>
9#include <string_view>
10
11#ifdef _WIN32
12# ifndef WIN32_LEAN_AND_MEAN
13# define WIN32_LEAN_AND_MEAN
14# endif
15# ifndef NOMINMAX
16# define NOMINMAX
17# endif
18# include <windows.h>
19#else
20# include <unistd.h>
21#endif
22
23#include "fe/assert.h"
24#include "fe/format.h"
25
26/// Lightweight stream-based terminal colors for diagnostics and CLI output.
27///
28/// Include `fe/term.h` and stream a @ref fe::term::FG value into an `std::ostream`:
29/// ```
30/// std::cerr << fe::term::FG::Red << "error: " << fe::term::FG::Reset << "unexpected token\n";
31/// ```
32///
33/// The current behavior is controlled via @ref fe::term::Mode and can be overridden with
34/// @ref fe::term::set_mode. In @ref fe::term::Mode::Auto, colors are emitted only for
35/// `std::cout`, `std::cerr`, `std::clog`, or streams sharing those buffers when they refer to
36/// terminals. FE also respects the common environment conventions `NO_COLOR`, `CLICOLOR=0`, and
37/// `CLICOLOR_FORCE` (unless it is set to `0`).
38///
39/// Note that a `std::formatter` never sees the destination stream - it formats into a detached buffer.
40/// Hence, embedding a @ref fe::term::FG value in a `std::format`/`std::print` format string resolves
41/// @ref fe::term::Mode::Auto to "no color".
42/// If you emit colors this way, call @ref fe::term::resolve_mode once at startup to decide
43/// @ref fe::term::Mode::Auto up front based on a representative stream.
44namespace fe::term {
45
46/// Controls whether color escape sequences are emitted.
47enum class Mode {
51};
52
53/// Foreground colors that can be streamed into an `std::ostream`.
66
67namespace detail {
68
69enum class Stream {
70 Unknown,
71 Stdout,
72 Stderr,
73};
74
75inline bool env_set(const char* name) noexcept {
76 auto* value = std::getenv(name);
77 return value && *value != '\0';
78}
79
80inline bool env_is(const char* name, const char* expected) noexcept {
81 auto* value = std::getenv(name);
82 return value && std::strcmp(value, expected) == 0;
83}
84
85inline Mode default_mode() noexcept {
86 if (env_set("NO_COLOR")) return Mode::Never;
87 if (env_set("CLICOLOR_FORCE") && !env_is("CLICOLOR_FORCE", "0")) return Mode::Always;
88 if (env_is("CLICOLOR", "0")) return Mode::Never;
89 return Mode::Auto;
90}
91
92inline std::atomic<Mode>& current_mode() noexcept {
93 static std::atomic<Mode> mode(default_mode());
94 return mode;
95}
96
97inline std::streambuf* stdout_rdbuf() noexcept {
98 static std::streambuf* buf = std::cout.rdbuf();
99 return buf;
100}
101
102inline std::streambuf* stderr_rdbuf() noexcept {
103 static std::streambuf* buf = std::cerr.rdbuf();
104 return buf;
105}
106
107inline std::streambuf* clog_rdbuf() noexcept {
108 static std::streambuf* buf = std::clog.rdbuf();
109 return buf;
110}
111
112inline Stream stream(std::ostream& os) noexcept {
113 auto* const buf = os.rdbuf();
114 if (buf == stdout_rdbuf()) return Stream::Stdout;
115 if (buf == stderr_rdbuf() || buf == clog_rdbuf()) return Stream::Stderr;
116 return Stream::Unknown;
117}
118
119#ifdef _WIN32
120inline bool enable_vt(HANDLE handle) noexcept {
121 if (handle == INVALID_HANDLE_VALUE) return false;
122
123 DWORD mode = 0;
124 if (!GetConsoleMode(handle, &mode)) return false;
125 if (mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING) return true;
126 return SetConsoleMode(handle, mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING) != 0;
127}
128
129inline bool is_terminal(Stream s) noexcept {
130 switch (s) {
131 case Stream::Stdout: {
132 static bool stdout_is_terminal = enable_vt(GetStdHandle(STD_OUTPUT_HANDLE));
133 return stdout_is_terminal;
134 }
135 case Stream::Stderr: {
136 static bool stderr_is_terminal = enable_vt(GetStdHandle(STD_ERROR_HANDLE));
137 return stderr_is_terminal;
138 }
139 default: return false;
140 }
141}
142#else
143inline bool is_terminal(Stream s) noexcept {
144 switch (s) {
145 case Stream::Stdout: {
146 static bool stdout_is_terminal = ::isatty(STDOUT_FILENO) != 0;
147 return stdout_is_terminal;
148 }
149 case Stream::Stderr: {
150 static bool stderr_is_terminal = ::isatty(STDERR_FILENO) != 0;
151 return stderr_is_terminal;
152 }
153 default: return false;
154 }
155}
156#endif
157
158inline bool use_color(std::ostream& os) noexcept {
159 // clang-format off
160 switch (current_mode().load(std::memory_order_relaxed)) {
161 case Mode::Always: return true;
162 case Mode::Never: return false;
163 case Mode::Auto: return is_terminal(stream(os));
164 default: fe::unreachable();
165 }
166 // clang-format on
167}
168
169constexpr std::string_view sgr(FG color) noexcept {
170 // clang-format off
171 switch (color) {
172 case FG::Black: return "\033[30m";
173 case FG::Red: return "\033[31m";
174 case FG::Green: return "\033[32m";
175 case FG::Yellow: return "\033[33m";
176 case FG::Blue: return "\033[34m";
177 case FG::Magenta: return "\033[35m";
178 case FG::Cyan: return "\033[36m";
179 case FG::Gray: return "\033[90m";
180 case FG::Reset: return "\033[39m";
181 default: fe::unreachable();
182 }
183 // clang-format on
184}
185
186} // namespace detail
187
188/// Returns the current terminal color mode.
189inline Mode mode() noexcept { return detail::current_mode().load(std::memory_order_relaxed); }
190
191/// Overrides the current terminal color mode.
192inline void set_mode(Mode m) noexcept { detail::current_mode().store(m, std::memory_order_relaxed); }
193
194/// Resolves Mode::Auto to Mode::Always or Mode::Never, depending on whether @p os refers to a terminal.
195/// A `std::formatter` cannot see its destination stream, so FG values embedded in a
196/// `std::format`/`std::print` format string never detect a terminal in Mode::Auto.
197/// Call this once at startup to make formatted output colored as well; explicit modes are left untouched:
198/// ```
199/// fe::term::resolve_mode(); // decide based on stderr
200/// std::print(std::cerr, "{}error:{} ...", fe::term::FG::Red, fe::term::FG::Reset);
201/// ```
202inline void resolve_mode(std::ostream& os = std::cerr) noexcept {
203 if (mode() == Mode::Auto) set_mode(detail::is_terminal(detail::stream(os)) ? Mode::Always : Mode::Never);
204}
205
206/// Streams the ANSI escape sequence for @p color when colors are enabled for @p os.
207inline std::ostream& operator<<(std::ostream& os, FG color) {
208 if (detail::use_color(os)) {
209 auto esc = detail::sgr(color);
210 os.write(esc.data(), esc.size());
211 }
212 return os;
213}
214
215} // namespace fe::term
216
217#ifndef DOXYGEN
218template<>
219struct std::formatter<fe::term::FG> : fe::ostream_formatter {};
220#endif
Lightweight stream-based terminal colors for diagnostics and CLI output.
Definition term.h:44
Mode mode() noexcept
Returns the current terminal color mode.
Definition term.h:189
FG
Foreground colors that can be streamed into an std::ostream.
Definition term.h:54
@ Magenta
Definition term.h:60
Mode
Controls whether color escape sequences are emitted.
Definition term.h:47
void resolve_mode(std::ostream &os=std::cerr) noexcept
Resolves Mode::Auto to Mode::Always or Mode::Never, depending on whether os refers to a terminal.
Definition term.h:202
std::ostream & operator<<(std::ostream &os, FG color)
Streams the ANSI escape sequence for color when colors are enabled for os.
Definition term.h:207
void set_mode(Mode m) noexcept
Overrides the current terminal color mode.
Definition term.h:192
Definition arena.h:13
basic_ostream_formatter< char > ostream_formatter
Definition format.h:64
void unreachable()
Definition assert.h:20