Skip to content

Commit

Permalink
Merge pull request #248 from rbrich/termctl-custom-format
Browse files Browse the repository at this point in the history
[core] TermCtl: A custom parser to replace color and mode placeholders in print/format
  • Loading branch information
rbrich authored Jul 14, 2024
2 parents cf9b37f + e862bf0 commit 274949e
Show file tree
Hide file tree
Showing 16 changed files with 201 additions and 206 deletions.
25 changes: 13 additions & 12 deletions examples/core/demo_termctl.cpp
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// demo_termctl.cpp created on 2018-07-11 as part of xcikit project
// https://github.com/rbrich/xcikit
//
// Copyright 2018, 2020 Radek Brich
// Copyright 2018–2024 Radek Brich
// Licensed under the Apache License, Version 2.0 (see LICENSE file)

#include <xci/core/TermCtl.h>
Expand All @@ -27,18 +27,19 @@ int main()

cout << t.move_up().move_right(6).bold().green() << "GREEN" <<t.normal() << endl;

t.print("{t:bold}{fg:yellow}formatted{t:normal}\n");
t.print("{t:bold}bold{t:normal_intensity} "
"{t:dim}dim{t:normal_intensity} "
"{t:italic}italic{t:no_italic} "
"{t:underline}underlined{t:no_underline} "
"{t:overline}overlined{t:no_overline} "
"{t:cross_out}crossed out{t:no_cross_out} "
"{t:frame}framed{t:no_frame} "
"{t:blink}blinking{t:no_blink} "
"{t:reverse}reversed{t:no_reverse} "
"{t:hidden}hidden{t:no_hidden} "
t.print("<b><yellow>formatted <*white><@yellow> bg <n>\n");
t.print("<bold>bold<normal_intensity> "
"<dim>dim<normal_intensity> "
"<italic>italic<no_italic> "
"<underline>underlined<no_underline> "
"<overline>overlined<no_overline> "
"<cross_out>crossed out<no_cross_out> "
"<frame>framed<no_frame> "
"<blink>blinking<no_blink> "
"<reverse>reversed<no_reverse> "
"<hidden>hidden<no_hidden> "
"\n");
t.print("Escaped \\<bold>. Unknown <tag>.\n");

t.tab_set_all({30, 20}).write();
t.print("tab stops:\t1\t2\n");
Expand Down
14 changes: 7 additions & 7 deletions src/xci/core/ArgParser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -136,12 +136,12 @@ std::string Option::usage() const
if (!p)
break;
if (is_remainder() && p.dashes == 2 && p.len == 0) {
res += t.format("[{fg:green}{}{t:normal}] ", std::string(dp + p.pos, p.dashes));
res += t.format("[<green>{}<normal>] ", std::string(dp + p.pos, p.dashes));
} else if (first) {
first = false;
res += t.format("{t:bold}{fg:green}{}{t:normal}", std::string(dp + p.pos, p.dashes + p.len));
res += t.format("<bold><green>{}<normal>", std::string(dp + p.pos, p.dashes + p.len));
} else if (!p.dashes) {
res += t.format(" {fg:green}{}{t:normal}", std::string(dp + p.pos, p.len));
res += t.format(" <green>{}<normal>", std::string(dp + p.pos, p.len));
}
dp += p.end();
}
Expand Down Expand Up @@ -312,7 +312,7 @@ ArgParser& ArgParser::operator()(const char* argv[], bool detect_width, unsigned
if (!parse_program_name(argv[0])) {
// this should not occur
auto& t = TermCtl::stderr_instance();
t.print("{t:bold}{fg:red}Missing program name (argv[0]){t:normal}\n");
t.print("<bold><red>Missing program name (argv[0])<normal>\n");
exit(1);
}
try {
Expand All @@ -326,7 +326,7 @@ ArgParser& ArgParser::operator()(const char* argv[], bool detect_width, unsigned
}
} catch (const BadArgument& e) {
auto& t = TermCtl::stderr_instance();
t.print("{t:bold}{fg:yellow}Error: {fg:red}{}{t:normal}\n\n", e.what());
t.print("<bold><yellow>Error: <red>{}<normal>\n\n", e.what());
print_usage();
print_help_notice();
exit(1);
Expand Down Expand Up @@ -525,7 +525,7 @@ void ArgParser::print_usage() const

unsigned indent = 0;
{
auto head = t.format("{t:bold}{fg:yellow}Usage:{t:normal} {t:bold}{}{t:normal} ", m_progname);
auto head = t.format("<bold><yellow>Usage:<normal> <bold>{}<normal> ", m_progname);
indent = TermCtl::stripped_width(head);
cout << head;
}
Expand All @@ -545,7 +545,7 @@ void ArgParser::print_help() const
desc_cols = std::max(desc_cols, (unsigned) opt.desc().size());
print_usage();
auto& t = TermCtl::stdout_instance();
t.print("\n{t:bold}{fg:yellow}Options:{t:normal}\n");
t.print("\n<bold><yellow>Options:<normal>\n");
for (const auto& opt : m_opts) {
cout << " " << opt.formatted_desc(desc_cols) << " ";
wrapping_print(opt.help(), desc_cols + 4, 0, m_max_width);
Expand Down
65 changes: 54 additions & 11 deletions src/xci/core/TermCtl.cpp
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// TermCtl.cpp created on 2018-07-09 as part of xcikit project
// https://github.com/rbrich/xcikit
//
// Copyright 2018, 2020, 2021 Radek Brich
// Copyright 2018–2024 Radek Brich
// Licensed under the Apache License, Version 2.0 (see LICENSE file)

// References:
Expand All @@ -24,6 +24,9 @@
#include <xci/core/file.h>
#include <xci/config.h>

#include <tao/pegtl.hpp>
namespace pegtl = tao::pegtl;

#ifdef _WIN32
static_assert(sizeof(unsigned long) == sizeof(DWORD));
#else
Expand Down Expand Up @@ -608,21 +611,61 @@ TermCtl& TermCtl::clear_line_to_end() { return TERM_APPEND(clr_eol); }
TermCtl& TermCtl::soft_reset() { return XCI_TERM_APPEND(seq::send_soft_reset); }


std::string TermCtl::FgPlaceholder::seq(Color color) const
{
return term_ctl->fg(color).seq();
}
namespace render_parser {

struct Char : pegtl::any {};
struct Escape : pegtl::seq< pegtl::one<'\\'>, Char > {};
struct Tag : pegtl::seq< pegtl::one<'<'>, pegtl::opt<pegtl::one<'@'>>, pegtl::opt<pegtl::one<'*'>>, pegtl::plus<pegtl::ranges<'a', 'z', '_'>>, pegtl::one<'>'>> {};
struct Grammar : pegtl::must< pegtl::star<pegtl::sor<Escape, Tag, Char>>, pegtl::eof > {};

std::string TermCtl::BgPlaceholder::seq(Color color) const
{
return term_ctl->bg(color).seq();
}
template< typename Rule >
struct Action {};

template<>
struct Action< Char > {
template< typename ParseInput >
static void apply( const ParseInput& in, TermCtl& t, std::string& r ) {
r.push_back(in.peek_char());
}
};

template<>
struct Action< Tag > {
template< typename ParseInput >
static bool apply( const ParseInput& in, TermCtl& t, std::string& r ) {
auto key = in.string_view().substr(1, in.size() - 2); // strip < >

const auto m = TermCtl::parse_mode(key);
if (m <= TermCtl::Mode::_Last) {
r += t.mode(m).seq();
return true;
}

bool is_bg = key.front() == '@';
if (is_bg)
key.remove_prefix(1);

const auto c = TermCtl::parse_color(key);
if (c <= TermCtl::Color::_Last) {
if (is_bg)
r += t.bg(c).seq();
else
r += t.fg(c).seq();
return true;
}

return false;
}
};

} // namespace render_parser

std::string TermCtl::ModePlaceholder::seq(Mode mode) const
std::string TermCtl::render(std::string_view markup)
{
return term_ctl->mode(mode).seq();
pegtl::memory_input in( std::to_address(markup.begin()), std::to_address(markup.end()) );
std::string r;
pegtl::parse< render_parser::Grammar, render_parser::Action >( in, *this, r );
return r;
}


Expand Down
141 changes: 46 additions & 95 deletions src/xci/core/TermCtl.h
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// TermCtl.h created on 2018-07-09 as part of xcikit project
// https://github.com/rbrich/xcikit
//
// Copyright 2018–2023 Radek Brich
// Copyright 2018–2024 Radek Brich
// Licensed under the Apache License, Version 2.0 (see LICENSE file)

#ifndef XCI_CORE_TERM_H
Expand Down Expand Up @@ -70,23 +70,33 @@ class TermCtl {
Invalid = 8, Default = 9,
BrightBlack = 10, BrightRed, BrightGreen, BrightYellow,
BrightBlue, BrightMagenta, BrightCyan, BrightWhite,
Last = BrightWhite
_Last = BrightWhite
};
static constexpr const char* c_color_names[] = {
"black", "red", "green", "yellow", "blue", "magenta", "cyan", "white",
"invalid", "default",
"*black", "*red", "*green", "*yellow",
"*blue", "*magenta", "*cyan", "*white"
};
static_assert(std::size(c_color_names) == size_t(Color::Last) + 1);
static_assert(std::size(c_color_names) == size_t(Color::_Last) + 1);

static constexpr Color parse_color(std::string_view name) {
Color r = Color::Black;
for (const char* n : c_color_names) {
if (name == n)
return r;
r = static_cast<Color>(uint8_t(r) + 1);
}
return r;
}

enum class Mode: uint8_t {
Normal, // reset all attributes
Bold, Dim, Italic, Underline, Overline, CrossOut, Frame,
Blink, Reverse, Hidden,
NormalIntensity, NoItalic, NoUnderline, NoOverline, NoCrossOut, NoFrame,
NoBlink, NoReverse, NoHidden,
Last = NoHidden
_Last = NoHidden
};
static constexpr const char* c_mode_names[] = {
"normal",
Expand All @@ -95,7 +105,17 @@ class TermCtl {
"normal_intensity", "no_italic", "no_underline", "no_overline", "no_cross_out", "no_frame",
"no_blink", "no_reverse", "no_hidden"
};
static_assert(std::size(c_mode_names) == size_t(Mode::Last) + 1);
static_assert(std::size(c_mode_names) == size_t(Mode::_Last) + 1);

static constexpr Mode parse_mode(std::string_view name) {
Mode r = Mode::Normal;
for (const char* n : c_mode_names) {
if (name == n || (name.size() == 1 && name[0] == n[0]))
return r;
r = static_cast<Mode>(uint8_t(r) + 1);
}
return r;
}

// foreground
TermCtl& fg(Color color);
Expand Down Expand Up @@ -194,74 +214,36 @@ class TermCtl {
TermCtl& soft_reset();

// Cached seq
std::string seq() { return std::move(m_seq); }
std::string seq() { auto s = std::move(m_seq); m_seq.clear(); return s; }
void write() { write_raw(seq()); }
void write_nl() { m_seq.append(1, '\n'); write(seq()); }
friend std::ostream& operator<<(std::ostream& os, TermCtl& t) { return os << t.seq(); }

// Formatting helpers
struct Placeholder {
TermCtl* term_ctl;
};
struct ColorPlaceholder: Placeholder {
using ValueType = Color;
static constexpr Color parse(std::string_view name) {
Color r = Color::Black;
for (const char* n : c_color_names) {
if (name == n)
return r;
r = static_cast<Color>(uint8_t(r) + 1);
}
if (r > Color::Last) // this condition is needed for GCC 10 to allow throw in constexpr
throw fmt::format_error("invalid color name: " + std::string(name));
XCI_UNREACHABLE;
}
};
struct FgPlaceholder: ColorPlaceholder {
std::string seq(Color color) const;
};
struct BgPlaceholder: ColorPlaceholder {
std::string seq(Color color) const;
};
struct ModePlaceholder: Placeholder {
using ValueType = Mode;
static constexpr Mode parse(std::string_view name) {
Mode r = Mode::Normal;
for (const char* n : c_mode_names) {
if (name == n)
return r;
r = static_cast<Mode>(uint8_t(r) + 1);
}
if (r > Mode::Last)
throw fmt::format_error("invalid mode name: " + std::string(name));
XCI_UNREACHABLE;
}
std::string seq(Mode mode) const;
};
/// Translate special markup language to color/mode control sequences:
/// <COLOR> where COLOR is default | red | *red ... ("*" = bright)
/// <@BG_COLOR> where BG_COLOR is the same as for COLOR
/// <MODE> where MODE is bold | underline | normal ... (shortcuts b | u | n ...)
std::string render(std::string_view markup);

/// Format string, adding colors via special placeholders:
/// {fg:COLOR} where COLOR is default | red | *red ... ("*" = bright)
/// {bg:COLOR} where COLOR is the same as for fg
/// {t:MODE} where MODE is bold | underline | normal ...
#define XCI_TERMCTL_FMT_DECL_ARGS decltype("fg"_a = FgPlaceholder{}), \
decltype("bg"_a = BgPlaceholder{}), \
decltype("t"_a = ModePlaceholder{})
#define XCI_TERMCTL_FMT_ARGS "fg"_a = FgPlaceholder{this}, \
"bg"_a = BgPlaceholder{this}, \
"t"_a = ModePlaceholder{this}
/// Format a string, adding colors via special placeholders - see `render` above.
/// Note that these placeholders are applied ahead of the <fmt> placeholders,
/// so e.g. `{}` cannot expand to `<bold>` which would be recognized. If this is
/// intended, call `fmt::format` separately and pass the result to `TermCtl::render`.
template<typename... T>
std::string format(fmt::format_string<T..., XCI_TERMCTL_FMT_DECL_ARGS> fmt, T&&... args) {
return _plain_format(fmt, args..., XCI_TERMCTL_FMT_ARGS);
std::string format(fmt::format_string<T...> fmt, T&&... args) {
const auto sv = fmt.get();
return fmt::vformat(render(std::string_view(sv.data(), sv.size())),
fmt::make_format_args(args...));
}

/// Print string with special color/mode placeholders, see `format` above.
/// Print string with special color/mode placeholders, see `render` above.
template<typename... T>
void print(fmt::format_string<T..., XCI_TERMCTL_FMT_DECL_ARGS> fmt, T&&... args) {
write(_plain_format(fmt, args..., XCI_TERMCTL_FMT_ARGS));
void print(fmt::format_string<T...> fmt, T&&... args) {
write(format(fmt::runtime(fmt), args...));
}
void print(std::string_view markup) {
write(render(markup));
}

#undef XCI_TERMCTL_FMT_DECL_ARGS
#undef XCI_TERMCTL_FMT_ARGS

void write(std::string_view buf);
void write_raw(std::string_view buf); // doesn't check newline
Expand Down Expand Up @@ -422,13 +404,8 @@ class TermCtl {
TermCtl& _append_seq(const char* seq) { if (seq) m_seq += seq; return *this; } // needed for TermInfo, which returns NULL for unknown seqs
TermCtl& _append_seq(std::string_view seq) { m_seq += seq; return *this; }

// helper to avoid fmt error on named arg not being lvalue
template<typename... T>
std::string _plain_format(fmt::string_view fmt, T&&... args) {
return fmt::vformat(fmt, fmt::make_format_args(args...));
}
std::string m_seq; // cached capability sequences
WriteCallback m_write_cb {};
WriteCallback m_write_cb;
int m_fd; // FD (on Windows mapped to handle)
bool m_tty_ok : 1 = false; // tty initialized, will reset the term when destroyed
bool m_at_newline : 1 = true;
Expand All @@ -441,32 +418,6 @@ class TermCtl {
} // namespace xci::core


template<typename T>
concept TermCtlPlaceholder =
std::is_same_v<T, xci::core::TermCtl::FgPlaceholder> ||
std::is_same_v<T, xci::core::TermCtl::BgPlaceholder> ||
std::is_same_v<T, xci::core::TermCtl::ModePlaceholder>;

template <TermCtlPlaceholder T>
struct [[maybe_unused]] fmt::formatter<T> {
typename T::ValueType value;
constexpr auto parse(format_parse_context& ctx) {
auto it = ctx.begin(); // NOLINT
while (it != ctx.end() && *it != '}') {
++it;
}
value = T::parse({ctx.begin(), size_t(it - ctx.begin())});
return it;
}

template <typename FormatContext>
auto format(const T& p, FormatContext& ctx) const {
auto msg = p.seq(value);
return std::copy(msg.begin(), msg.end(), ctx.out());
}
};


// support `term.bold()` etc. directly in format args
template <>
struct [[maybe_unused]] fmt::formatter<xci::core::TermCtl> {
Expand Down
Loading

0 comments on commit 274949e

Please sign in to comment.