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