FE 0.15.0
A C++23 toolkit for writing compiler/interpreter frontends.
Loading...
Searching...
No Matches
term.h
Go to the documentation of this file.
1#pragma once
2
3#include <iostream>
4#include <optional>
5#include <sstream>
6#include <stdexcept>
7#include <string>
8#include <string_view>
9#include <type_traits>
10
11#include "fe/api.h"
12#include "fe/assert.h"
13#include "fe/format.h"
14#include "fe/restore.h"
15
16/// Lightweight stream-based terminal colors for diagnostics and CLI output.
17///
18/// Include `fe/term.h` and stream a @ref fe::term::FG value into an `std::ostream`:
19/// ```
20/// std::cerr << fe::term::FG::Red << "error: " << fe::term::FG::Reset << "unexpected token\n";
21/// ```
22///
23/// The current behavior is controlled via @ref fe::term::Mode and can be overridden with
24/// @ref fe::term::set_mode. In @ref fe::term::Mode::Auto, colors are emitted only for
25/// `std::cout`, `std::cerr`, `std::clog`, or streams sharing those buffers when they refer to
26/// terminals. FE also respects the common environment conventions `NO_COLOR`, `CLICOLOR=0`, and
27/// `CLICOLOR_FORCE` (unless it is set to `0`).
28///
29/// Note that a `std::formatter` never sees the destination stream - it formats into a detached buffer.
30/// Hence, embedding a @ref fe::term::FG value in a `std::format`/`std::print` format string resolves
31/// @ref fe::term::Mode::Auto to "no color".
32/// If you emit colors this way, call @ref fe::term::resolve_mode once at startup to decide
33/// @ref fe::term::Mode::Auto up front based on a representative stream.
34///
35/// The mode and @ref fe::term::auto_detached live in the fe library rather than in this header, so a
36/// shared library loaded via fe::dl sees whatever the host set instead of starting over from the defaults.
37///
38/// Use @ref fe::term::use_color to branch on whether color will actually be emitted, e.g. to keep a
39/// plain-text fallback in sync with the colored rendering.
40namespace fe::term {
41
42/// Controls whether color escape sequences are emitted.
43enum class Mode {
47};
48
49/// Foreground colors that can be streamed into an `std::ostream`.
62
63namespace detail {
64
65enum class Stream {
66 Unknown,
67 Stdout,
68 Stderr,
69};
70
71/// Which of the standard streams @p os writes to, if any.
72FE_API Stream stream(std::ostream& os) noexcept;
73
74/// Does @p s refer to a terminal? Decided once per stream and cached.
75FE_API bool is_terminal(Stream s) noexcept;
76
77constexpr std::string_view sgr(FG color) noexcept {
78 // clang-format off
79 switch (color) {
80 case FG::Black: return "\033[30m";
81 case FG::Red: return "\033[31m";
82 case FG::Green: return "\033[32m";
83 case FG::Yellow: return "\033[33m";
84 case FG::Blue: return "\033[34m";
85 case FG::Magenta: return "\033[35m";
86 case FG::Cyan: return "\033[36m";
87 case FG::Gray: return "\033[90m";
88 case FG::Reset: return "\033[39m";
89 default: fe::unreachable();
90 }
91 // clang-format on
92}
93
94/// Does @p str spell an escape - `` \` `` or `\\` - at position @p i of `[i, end)`?
95constexpr bool escape(std::string_view str, size_t i, size_t end) noexcept {
96 return str[i] == '\\' && i + 1 != end && (str[i + 1] == '`' || str[i + 1] == '\\');
97}
98
99/// Index of the next backtick at or after @p i that is not escaped, or `npos`.
100FE_API size_t tick(std::string_view str, size_t i) noexcept;
101
102/// Streams `[begin, end)` of @p str, dropping the leading backslash of every escape.
103FE_API void stream_raw(std::ostream& os, std::string_view str, size_t begin, size_t end);
104
105/// Columns `[begin, end)` of @p str occupies once streamed via stream_raw - one less per escape.
106FE_API size_t raw_width(std::string_view str, size_t begin, size_t end) noexcept;
107
108/// Splits @p str into its `` `citation` `` markup and invokes `f(begin, end, cited)` on each piece;
109/// an unpaired backtick is no citation. This is the one place that knows the grammar.
110void scan_cite(std::string_view str, auto&& f) {
111 for (size_t i = 0, e = str.size(); i != e;) {
112 auto l = tick(str, i);
113 auto r = l == std::string_view::npos ? l : tick(str, l + 1);
114 if (r == std::string_view::npos) return f(i, e, false);
115
116 f(i, l, false);
117 f(l + 1, r, true);
118 i = r + 1;
119 }
120}
121
122} // namespace detail
123
124/// Returns the current terminal color mode.
125FE_API Mode mode() noexcept;
126
127/// Whether Mode::Auto emits colors into a detached buffer - anything a `std::formatter` writes into.
128/// @ref resolve_mode is the usual way to decide this.
129FE_API bool auto_detached() noexcept;
130
131/// Overrides @ref auto_detached.
132FE_API void set_auto_detached(bool b) noexcept;
133
134/// Whether color escape sequences are emitted for @p os right now.
135/// In Mode::Auto this is decided by whether @p os refers to a terminal, while a detached buffer
136/// (anything a `std::formatter` writes into) follows @ref resolve_mode.
137/// Use this to keep a plain-text fallback in sync with what @ref operator<<(std::ostream&, FG) will emit,
138/// e.g. to spell out a marker only when it cannot be conveyed by color.
139FE_API bool use_color(std::ostream& os) noexcept;
140
141/// Number of columns of the terminal @p os refers to.
142/// Unlike @ref use_color, this ignores @ref Mode and always asks the actual stream.
143/// @returns `std::nullopt` if @p os is not a terminal or its size cannot be determined.
144FE_API std::optional<size_t> width(std::ostream& os) noexcept;
145
146/// Overrides the current terminal color mode.
147FE_API void set_mode(Mode m) noexcept;
148
149/// Overrides the color mode for the duration of the scope.
150/// @warning The mode is global, so this affects every stream - and every thread - while it is alive.
152
153/// Decides Mode::Auto for detached buffers, depending on whether @p os refers to a terminal.
154/// A `std::formatter` cannot see its destination stream, so FG values embedded in a
155/// `std::format`/`std::print` format string never detect a terminal on their own.
156/// Call this once at startup to make formatted output colored as well:
157/// ```
158/// fe::term::resolve_mode(); // decide based on stderr
159/// std::print(std::cerr, "{}error:{} ...", fe::term::FG::Red, fe::term::FG::Reset);
160/// ```
161/// A real stream still decides on its own, and explicit modes are left untouched.
162FE_API void resolve_mode(std::ostream& os = std::cerr) noexcept;
163
164/// Streams the ANSI escape sequence for @p color when colors are enabled for @p os.
165FE_API std::ostream& operator<<(std::ostream& os, FG color);
166
167/// Escapes @p str into @p out so that render_cite reproduces it verbatim instead of reading it as markup.
168/// Escaping the backslashes as well keeps a trailing one from swallowing the backtick that follows it.
169template<class O>
170O escape_cite_to(O out, std::string_view str) {
171 for (auto c : str) {
172 if (c == '`' || c == '\\') *out++ = '\\';
173 *out++ = c;
174 }
175 return out;
176}
177
178/// As above but into a fresh `std::string`.
179FE_API std::string escape_cite(std::string_view str);
180
181/// An *owning* message fragment that spells `` `citations` `` of its own; what format_cite yields.
182/// Return this from a helper that assembles a fragment, and it stays valid wherever a Cite may go.
183class Cited {
184public:
185 explicit Cited(std::string str) noexcept
186 : str_(std::move(str)) {}
187
188 [[nodiscard]] constexpr std::string_view view() const noexcept { return str_; }
189 constexpr operator std::string_view() const noexcept { return str_; }
190
191private:
192 std::string str_;
193};
194
195/// A *borrowed* fragment whose backticks stay markup; format_cite escapes every other argument.
196/// A literal and a Cited convert implicitly - both are text someone wrote as markup.
197/// Runtime text has to say so: spell it `Cite(s)`, so data never becomes markup by accident.
198/// A `const char*` - a ternary of two literals, say - counts as runtime text and needs `Cite(s)` too.
199/// @warning Borrows its text like a `std::string_view` does - a Cited outlives the expression, a Cite does not.
200class Cite {
201public:
202 constexpr Cite() noexcept = default; ///< The empty fragment - a context that says nothing.
203 template<size_t N>
204 constexpr Cite(const char (&s)[N]) noexcept
205 : str_(s) {}
206 constexpr Cite(const Cited& cited) noexcept
207 : str_(cited) {}
208 constexpr explicit Cite(std::string_view s) noexcept
209 : str_(s) {}
210
211 [[nodiscard]] constexpr std::string_view view() const noexcept { return str_; }
212 [[nodiscard]] constexpr bool empty() const noexcept { return str_.empty(); }
213 constexpr explicit operator bool() const noexcept { return !str_.empty(); } ///< Is not empty?
214
215private:
216 std::string_view str_;
217};
218
219namespace detail {
220
221/// Wraps an argument whose backticks are data and must not delimit a citation.
222template<class T>
223struct Escaped {
224 const T& val;
225};
226
227template<class T, class U = std::remove_cvref_t<T>>
228using cite_arg_t = std::conditional_t<std::is_same_v<U, Cite> || std::is_same_v<U, Cited>, Cite, Escaped<U>>;
229
230/// std::vformat, but each argument renders as data instead of as markup; see Cite for the exception.
231/// @note The wrappers are lambda *parameters* because `std::make_format_args` does not bind rvalues.
232template<class... Args>
233std::string vformat_cite(std::string_view fmt, const Args&... args) {
234 auto vformat = [fmt]<class... A>(A... a) { return std::vformat(fmt, std::make_format_args(a...)); };
235 return vformat(cite_arg_t<Args>{args}...);
236}
237
238} // namespace detail
239
240/// A std::format_string whose backticks delimit a `` `citation` `` while those of its arguments are data.
241template<class... Args>
242using cite_string = std::format_string<detail::cite_arg_t<Args>...>;
243
244/// std::format for a message that follows that convention; assemble a fragment of your own with it.
245/// The Cited it yields is markup wherever it is used as an argument again - no re-wrapping needed.
246template<class... Args>
248 return Cited(detail::vformat_cite(fmt.get(), args...));
249}
250
251/// Streams @p str into @p os, coloring each `` `citation` `` and dropping its backticks - or keeping them
252/// verbatim without color; `` \` `` is a literal backtick and `\\` a literal backslash. This is the convention
253/// fe::CodeDiag renders a diagnostic message with; use it to apply the same convention elsewhere, e.g.
254/// fe::Cli::help.
255FE_API void render_cite(std::ostream& os, std::string_view str, bool color);
256
257/// As above but lets @p os decide the coloring; mirrors cite_width.
258FE_API void render_cite(std::ostream& os, std::string_view str);
259
260/// Number of columns @p str actually occupies once render_cite renders it with @p color - fewer than
261/// `str.size()` by the backticks/backslashes render_cite drops.
262FE_API size_t cite_width(std::string_view str, bool color);
263
264} // namespace fe::term
265
266namespace fe {
267/// The `` `citation` `` convention lives in fe::term, next to the renderer that reads it.
268using term::Cite; ///< @copydoc fe::term::Cite
269using term::cite_string; ///< @copydoc fe::term::cite_string
270using term::Cited; ///< @copydoc fe::term::Cited
271using term::format_cite; ///< @copydoc fe::term::format_cite
272
273/// Throws a `T` (a `std::logic_error` by default) whose message is `format_cite(fmt, args...)`.
274/// Use this for unrecoverable errors that should surface as a proper exception with a formatted message.
275/// The message is rendered here, so - like fe::Error::Bail - the `what()` is finished text a generic
276/// handler may print as is, not markup someone else still has to resolve.
277/// Colors follow term::auto_detached, just like the message fe::Log builds in a detached buffer.
278template<class T = std::logic_error, class... Args>
279[[noreturn]] void throwf(cite_string<Args...> fmt, Args&&... args) {
280 auto oss = std::ostringstream();
281 oss << term::FG::Red << "error: " << term::FG::Reset;
282 term::render_cite(oss, term::detail::vformat_cite(fmt.get(), args...));
283 throw T(oss.str());
284}
285} // namespace fe
286
287#ifndef DOXYGEN
288template<class T>
289struct std::formatter<fe::term::detail::Escaped<T>> {
290 std::string_view spec; ///< Borrowed from the format string, which outlives the `vformat` call.
291
292 constexpr auto parse(std::format_parse_context& ctx) {
293 auto i = ctx.begin();
294 for (; i != ctx.end() && *i != '}'; ++i)
295 if (*i == '{') throw std::format_error("fe::term::format_cite: a nested replacement field needs Cite");
296 spec = std::string_view(ctx.begin(), i);
297 return i;
298 }
299
300 auto format(const fe::term::detail::Escaped<T>& escaped, std::format_context& ctx) const {
301 auto str = spec.empty() ? std::format("{}", escaped.val)
302 : std::vformat(std::format("{{:{}}}", spec), std::make_format_args(escaped.val));
303 return fe::term::escape_cite_to(ctx.out(), str);
304 }
305};
306
307template<>
308struct std::formatter<fe::term::Cite> : std::formatter<std::string_view> {
309 auto format(fe::term::Cite cite, std::format_context& ctx) const {
310 return std::formatter<std::string_view>::format(cite.view(), ctx);
311 }
312};
313
314template<>
315struct std::formatter<fe::term::FG> : fe::ostream_formatter {};
316#endif
A borrowed fragment whose backticks stay markup; format_cite escapes every other argument.
Definition term.h:200
constexpr Cite() noexcept=default
The empty fragment - a context that says nothing.
Cited(std::string str) noexcept
Definition term.h:185
RAII guard that restores a value at the end of the scope.
Definition restore.h:10
A borrowed fragment whose backticks stay markup; format_cite escapes every other argument.
Definition term.h:200
constexpr Cite(const Cited &cited) noexcept
Definition term.h:206
constexpr Cite(std::string_view s) noexcept
Definition term.h:208
constexpr bool empty() const noexcept
Definition term.h:212
constexpr Cite() noexcept=default
The empty fragment - a context that says nothing.
constexpr std::string_view view() const noexcept
Definition term.h:211
An owning message fragment that spells `citations` of its own; what format_cite yields.
Definition term.h:183
constexpr std::string_view view() const noexcept
Definition term.h:188
Cited(std::string str) noexcept
Definition term.h:185
Lightweight stream-based terminal colors for diagnostics and CLI output.
Definition term.h:40
FE_API std::string escape_cite(std::string_view str)
As above but into a fresh std::string.
FE_API size_t cite_width(std::string_view str, bool color)
Number of columns str actually occupies once render_cite renders it with color - fewer than str....
Restore< Mode, &mode, &set_mode > ScopedMode
Overrides the color mode for the duration of the scope.
Definition term.h:151
FG
Foreground colors that can be streamed into an std::ostream.
Definition term.h:50
@ Magenta
Definition term.h:56
Mode
Controls whether color escape sequences are emitted.
Definition term.h:43
FE_API void set_mode(Mode m) noexcept
Overrides the current terminal color mode.
FE_API bool use_color(std::ostream &os) noexcept
Whether color escape sequences are emitted for os right now.
FE_API void resolve_mode(std::ostream &os=std::cerr) noexcept
Decides Mode::Auto for detached buffers, depending on whether os refers to a terminal.
Cited format_cite(cite_string< Args... > fmt, Args &&... args)
std::format for a message that follows that convention; assemble a fragment of your own with it.
Definition term.h:247
std::format_string< detail::cite_arg_t< Args >... > cite_string
A std::format_string whose backticks delimit a `citation` while those of its arguments are data.
Definition term.h:242
O escape_cite_to(O out, std::string_view str)
Escapes str into out so that render_cite reproduces it verbatim instead of reading it as markup.
Definition term.h:170
FE_API std::optional< size_t > width(std::ostream &os) noexcept
Number of columns of the terminal os refers to.
FE_API Mode mode() noexcept
Returns the current terminal color mode.
FE_API bool auto_detached() noexcept
Whether Mode::Auto emits colors into a detached buffer - anything a std::formatter writes into.
FE_API void render_cite(std::ostream &os, std::string_view str, bool color)
Streams str into os, coloring each `citation` and dropping its backticks - or keeping them verbatim...
FE_API void set_auto_detached(bool b) noexcept
Overrides auto_detached.
Definition algo.h:17
void throwf(cite_string< Args... > fmt, Args &&... args)
<
Definition term.h:279
basic_ostream_formatter< char > ostream_formatter
Definition format.h:61
void unreachable()
Definition assert.h:20
Definition span.h:150