FE 0.13.1
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 <optional>
9#include <ostream>
10#include <string_view>
11
12#ifdef _WIN32
13# ifndef WIN32_LEAN_AND_MEAN
14# define WIN32_LEAN_AND_MEAN
15# endif
16# ifndef NOMINMAX
17# define NOMINMAX
18# endif
19# include <windows.h>
20#else
21# include <sys/ioctl.h>
22# include <unistd.h>
23#endif
24
25#include "fe/assert.h"
26#include "fe/format.h"
27#include "fe/restore.h"
28
29/// Lightweight stream-based terminal colors for diagnostics and CLI output.
30///
31/// Include `fe/term.h` and stream a @ref fe::term::FG value into an `std::ostream`:
32/// ```
33/// std::cerr << fe::term::FG::Red << "error: " << fe::term::FG::Reset << "unexpected token\n";
34/// ```
35///
36/// The current behavior is controlled via @ref fe::term::Mode and can be overridden with
37/// @ref fe::term::set_mode. In @ref fe::term::Mode::Auto, colors are emitted only for
38/// `std::cout`, `std::cerr`, `std::clog`, or streams sharing those buffers when they refer to
39/// terminals. FE also respects the common environment conventions `NO_COLOR`, `CLICOLOR=0`, and
40/// `CLICOLOR_FORCE` (unless it is set to `0`).
41///
42/// Note that a `std::formatter` never sees the destination stream - it formats into a detached buffer.
43/// Hence, embedding a @ref fe::term::FG value in a `std::format`/`std::print` format string resolves
44/// @ref fe::term::Mode::Auto to "no color".
45/// If you emit colors this way, call @ref fe::term::resolve_mode once at startup to decide
46/// @ref fe::term::Mode::Auto up front based on a representative stream.
47///
48/// Use @ref fe::term::use_color to branch on whether color will actually be emitted, e.g. to keep a
49/// plain-text fallback in sync with the colored rendering.
50namespace fe::term {
51
52/// Controls whether color escape sequences are emitted.
53enum class Mode {
57};
58
59/// Foreground colors that can be streamed into an `std::ostream`.
72
73namespace detail {
74
75enum class Stream {
76 Unknown,
77 Stdout,
78 Stderr,
79};
80
81inline bool env_set(const char* name) noexcept {
82 auto* value = std::getenv(name);
83 return value && *value != '\0';
84}
85
86inline bool env_is(const char* name, const char* expected) noexcept {
87 auto* value = std::getenv(name);
88 return value && std::strcmp(value, expected) == 0;
89}
90
91inline Mode default_mode() noexcept {
92 if (env_set("NO_COLOR")) return Mode::Never;
93 if (env_set("CLICOLOR_FORCE") && !env_is("CLICOLOR_FORCE", "0")) return Mode::Always;
94 if (env_is("CLICOLOR", "0")) return Mode::Never;
95 return Mode::Auto;
96}
97
98inline std::atomic<Mode>& current_mode() noexcept {
99 static std::atomic<Mode> mode(default_mode());
100 return mode;
101}
102
103inline std::streambuf* stdout_rdbuf() noexcept {
104 static std::streambuf* buf = std::cout.rdbuf();
105 return buf;
106}
107
108inline std::streambuf* stderr_rdbuf() noexcept {
109 static std::streambuf* buf = std::cerr.rdbuf();
110 return buf;
111}
112
113inline std::streambuf* clog_rdbuf() noexcept {
114 static std::streambuf* buf = std::clog.rdbuf();
115 return buf;
116}
117
118inline Stream stream(std::ostream& os) noexcept {
119 auto* const buf = os.rdbuf();
120 if (buf == stdout_rdbuf()) return Stream::Stdout;
121 if (buf == stderr_rdbuf() || buf == clog_rdbuf()) return Stream::Stderr;
122 return Stream::Unknown;
123}
124
125#ifdef _WIN32
126inline bool enable_vt(HANDLE handle) noexcept {
127 if (handle == INVALID_HANDLE_VALUE) return false;
128
129 DWORD mode = 0;
130 if (!GetConsoleMode(handle, &mode)) return false;
131 if (mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING) return true;
132 return SetConsoleMode(handle, mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING) != 0;
133}
134
135inline bool is_terminal(Stream s) noexcept {
136 switch (s) {
137 case Stream::Stdout: {
138 static bool stdout_is_terminal = enable_vt(GetStdHandle(STD_OUTPUT_HANDLE));
139 return stdout_is_terminal;
140 }
141 case Stream::Stderr: {
142 static bool stderr_is_terminal = enable_vt(GetStdHandle(STD_ERROR_HANDLE));
143 return stderr_is_terminal;
144 }
145 default: return false;
146 }
147}
148#else
149inline bool is_terminal(Stream s) noexcept {
150 switch (s) {
151 case Stream::Stdout: {
152 static bool stdout_is_terminal = ::isatty(STDOUT_FILENO) != 0;
153 return stdout_is_terminal;
154 }
155 case Stream::Stderr: {
156 static bool stderr_is_terminal = ::isatty(STDERR_FILENO) != 0;
157 return stderr_is_terminal;
158 }
159 default: return false;
160 }
161}
162#endif
163
164constexpr std::string_view sgr(FG color) noexcept {
165 // clang-format off
166 switch (color) {
167 case FG::Black: return "\033[30m";
168 case FG::Red: return "\033[31m";
169 case FG::Green: return "\033[32m";
170 case FG::Yellow: return "\033[33m";
171 case FG::Blue: return "\033[34m";
172 case FG::Magenta: return "\033[35m";
173 case FG::Cyan: return "\033[36m";
174 case FG::Gray: return "\033[90m";
175 case FG::Reset: return "\033[39m";
176 default: fe::unreachable();
177 }
178 // clang-format on
179}
180
181} // namespace detail
182
183/// Returns the current terminal color mode.
184inline Mode mode() noexcept { return detail::current_mode().load(std::memory_order_relaxed); }
185
186/// Whether color escape sequences are emitted for @p os right now.
187/// In Mode::Auto this is decided by whether @p os refers to a terminal, so a detached buffer
188/// (anything a `std::formatter` writes into) yields `false`; see @ref resolve_mode.
189/// Use this to keep a plain-text fallback in sync with what @ref operator<<(std::ostream&, FG) will emit,
190/// e.g. to spell out a marker only when it cannot be conveyed by color.
191inline bool use_color(std::ostream& os) noexcept {
192 // clang-format off
193 switch (mode()) {
194 case Mode::Always: return true;
195 case Mode::Never: return false;
196 case Mode::Auto: return detail::is_terminal(detail::stream(os));
197 default: fe::unreachable();
198 }
199 // clang-format on
200}
201
202/// Number of columns of the terminal @p os refers to.
203/// Unlike @ref use_color, this ignores @ref Mode and always asks the actual stream.
204/// @returns `std::nullopt` if @p os is not a terminal or its size cannot be determined.
205inline std::optional<size_t> width(std::ostream& os) noexcept {
206 auto s = detail::stream(os);
207 if (!detail::is_terminal(s)) return {};
208
209#ifdef _WIN32
210 auto handle = GetStdHandle(s == detail::Stream::Stdout ? STD_OUTPUT_HANDLE : STD_ERROR_HANDLE);
211 if (CONSOLE_SCREEN_BUFFER_INFO info; GetConsoleScreenBufferInfo(handle, &info)) {
212 if (auto cols = info.srWindow.Right - info.srWindow.Left + 1; cols > 0) return size_t(cols);
213 }
214#else
215 auto fd = s == detail::Stream::Stdout ? STDOUT_FILENO : STDERR_FILENO;
216 if (winsize ws; ::ioctl(fd, TIOCGWINSZ, &ws) == 0 && ws.ws_col > 0) return size_t(ws.ws_col);
217#endif
218 return {};
219}
220
221/// Overrides the current terminal color mode.
222inline void set_mode(Mode m) noexcept { detail::current_mode().store(m, std::memory_order_relaxed); }
223
224/// Overrides the color mode for the duration of the scope.
225/// @warning The mode is global, so this affects every stream - and every thread - while it is alive.
227
228/// Resolves Mode::Auto to Mode::Always or Mode::Never, depending on whether @p os refers to a terminal.
229/// A `std::formatter` cannot see its destination stream, so FG values embedded in a
230/// `std::format`/`std::print` format string never detect a terminal in Mode::Auto.
231/// Call this once at startup to make formatted output colored as well; explicit modes are left untouched:
232/// ```
233/// fe::term::resolve_mode(); // decide based on stderr
234/// std::print(std::cerr, "{}error:{} ...", fe::term::FG::Red, fe::term::FG::Reset);
235/// ```
236inline void resolve_mode(std::ostream& os = std::cerr) noexcept {
237 if (mode() == Mode::Auto) set_mode(detail::is_terminal(detail::stream(os)) ? Mode::Always : Mode::Never);
238}
239
240/// Streams the ANSI escape sequence for @p color when colors are enabled for @p os.
241inline std::ostream& operator<<(std::ostream& os, FG color) {
242 if (use_color(os)) {
243 auto esc = detail::sgr(color);
244 os.write(esc.data(), esc.size());
245 }
246 return os;
247}
248
249} // namespace fe::term
250
251#ifndef DOXYGEN
252template<>
253struct std::formatter<fe::term::FG> : fe::ostream_formatter {};
254#endif
RAII guard that restores a value at the end of the scope.
Definition restore.h:10
Lightweight stream-based terminal colors for diagnostics and CLI output.
Definition term.h:50
Mode mode() noexcept
Returns the current terminal color mode.
Definition term.h:184
Restore< Mode, &mode, &set_mode > ScopedMode
Overrides the color mode for the duration of the scope.
Definition term.h:226
FG
Foreground colors that can be streamed into an std::ostream.
Definition term.h:60
@ Magenta
Definition term.h:66
Mode
Controls whether color escape sequences are emitted.
Definition term.h:53
std::optional< size_t > width(std::ostream &os) noexcept
Number of columns of the terminal os refers to.
Definition term.h:205
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:236
std::ostream & operator<<(std::ostream &os, FG color)
Streams the ANSI escape sequence for color when colors are enabled for os.
Definition term.h:241
bool use_color(std::ostream &os) noexcept
Whether color escape sequences are emitted for os right now.
Definition term.h:191
void set_mode(Mode m) noexcept
Overrides the current terminal color mode.
Definition term.h:222
Definition algo.h:17
basic_ostream_formatter< char > ostream_formatter
Definition format.h:64
void unreachable()
Definition assert.h:31