Browse Source

Output styling library

default_compile_flags
vector-of-bool 3 years ago
parent
commit
ca12ed4fac
15 changed files with 494 additions and 17 deletions
  1. +9
    -2
      src/dds/build/builder.cpp
  2. +6
    -1
      src/dds/build/plan/archive.cpp
  3. +5
    -2
      src/dds/build/plan/compile_exec.cpp
  4. +8
    -3
      src/dds/build/plan/exe.cpp
  5. +3
    -2
      src/dds/build/plan/exe.hpp
  6. +10
    -5
      src/dds/cli/cmd/pkg_repo_err_handle.cpp
  7. +4
    -1
      src/dds/error/nonesuch.cpp
  8. +6
    -1
      src/dds/pkg/remote.cpp
  9. +35
    -0
      src/fansi/style.hpp
  10. +173
    -0
      src/fansi/styled.cpp
  11. +34
    -0
      src/fansi/styled.hpp
  12. +34
    -0
      src/fansi/styled.test.cpp
  13. +101
    -0
      src/fansi/writer.cpp
  14. +57
    -0
      src/fansi/writer.hpp
  15. +9
    -0
      src/fansi/writer.test.cpp

+ 9
- 2
src/dds/build/builder.cpp View File

@@ -10,10 +10,13 @@
#include <dds/util/output.hpp>
#include <dds/util/time.hpp>

#include <fansi/styled.hpp>

#include <array>
#include <set>

using namespace dds;
using namespace fansi::literals;

namespace {

@@ -23,12 +26,16 @@ struct state {
};

void log_failure(const test_failure& fail) {
dds_log(error, "Test '{}' failed! [exited {}]", fail.executable_path.string(), fail.retc);
dds_log(error,
"Test .br.yellow[{}] .br.red[{}] [Exited {}]"_styled,
fail.executable_path.string(),
fail.timed_out ? "TIMED OUT" : "FAILED",
fail.retc);
if (fail.signal) {
dds_log(error, "Test execution received signal {}", fail.signal);
}
if (trim_view(fail.output).empty()) {
dds_log(error, "(Test executable produced no output");
dds_log(error, "(Test executable produced no output)");
} else {
dds_log(error, "Test output:\n{}[dds - test output end]", fail.output);
}

+ 6
- 1
src/dds/build/plan/archive.cpp View File

@@ -5,10 +5,12 @@
#include <dds/util/log.hpp>
#include <dds/util/time.hpp>

#include <fansi/styled.hpp>
#include <range/v3/range/conversion.hpp>
#include <range/v3/view/transform.hpp>

using namespace dds;
using namespace fansi::literals;

fs::path create_archive_plan::calc_archive_file_path(const toolchain& tc) const noexcept {
return _subdir / fmt::format("{}{}{}", "lib", _name, tc.archive_suffix());
@@ -55,7 +57,10 @@ void create_archive_plan::archive(const build_env& env) const {
"Creating static library archive [{}] failed for '{}'",
out_relpath,
_qual_name);
dds_log(error, "Subcommand FAILED: {}\n{}", quote_command(ar_cmd), ar_res.output);
dds_log(error,
"Subcommand FAILED: .bold.yellow[{}]\n{}"_styled,
quote_command(ar_cmd),
ar_res.output);
throw_external_error<
errc::archive_failure>("Creating static library archive [{}] failed for '{}'",
out_relpath,

+ 5
- 2
src/dds/build/plan/compile_exec.cpp View File

@@ -8,6 +8,7 @@
#include <dds/util/string.hpp>
#include <dds/util/time.hpp>

#include <fansi/styled.hpp>
#include <neo/assert.hpp>
#include <range/v3/range/conversion.hpp>
#include <range/v3/view/filter.hpp>
@@ -20,6 +21,7 @@

using namespace dds;
using namespace ranges;
using namespace fansi::literals;

namespace {

@@ -51,7 +53,8 @@ do_compile(const compile_file_full& cf, build_env_ref env, compile_counter& coun

// Generate a log message to display to the user
auto source_path = cf.plan.source_path();
auto msg = fmt::format("[{}] Compile: {}",

auto msg = fmt::format("[{}] Compile: .br.cyan[{}]"_styled,
cf.plan.qualifier(),
fs::relative(source_path, cf.plan.source().basis_path).string());

@@ -141,7 +144,7 @@ do_compile(const compile_file_full& cf, build_env_ref env, compile_counter& coun
if (!compiled_okay) {
dds_log(error, "Compilation failed: {}", source_path.string());
dds_log(error,
"Subcommand FAILED [Exitted {}]: {}\n{}",
"Subcommand .bold.red[FAILED] [Exited {}]: .bold.yellow[{}]\n{}"_styled,
compile_retc,
quote_command(cf.cmd_info.command),
compiler_output);

+ 8
- 3
src/dds/build/plan/exe.cpp View File

@@ -7,10 +7,13 @@
#include <dds/util/log.hpp>
#include <dds/util/time.hpp>

#include <fansi/styled.hpp>

#include <algorithm>
#include <chrono>

using namespace dds;
using namespace fansi::literals;

fs::path link_executable_plan::calc_executable_path(build_env_ref env) const noexcept {
return env.output_root / _out_subdir / (_name + env.toolchain.executable_suffix());
@@ -77,25 +80,27 @@ bool link_executable_plan::is_test() const noexcept {

std::optional<test_failure> link_executable_plan::run_test(build_env_ref env) const {
auto exe_path = calc_executable_path(env);
auto msg = fmt::format("Run test: {:30}", fs::relative(exe_path, env.output_root).string());
auto msg = fmt::format("Run test: .br.cyan[{:30}]"_styled,
fs::relative(exe_path, env.output_root).string());
dds_log(info, msg);
using namespace std::chrono_literals;
auto&& [dur, res] = timed<std::chrono::microseconds>(
[&] { return run_proc({.command = {exe_path.string()}, .timeout = 10s}); });

if (res.okay()) {
dds_log(info, "{} - PASSED - {:>9L}μs", msg, dur.count());
dds_log(info, "{} - .br.green[PASS] - {:>9L}μs", msg, dur.count());
return std::nullopt;
} else {
auto exit_msg = fmt::format(res.signal ? "signalled {}" : "exited {}",
res.signal ? res.signal : res.retc);
auto fail_str = res.timed_out ? "TIMEOUT" : "FAILED ";
auto fail_str = res.timed_out ? ".br.yellow[TIME]"_styled : ".br.red[FAIL]"_styled;
dds_log(error, "{} - {} - {:>9L}μs [{}]", msg, fail_str, dur.count(), exit_msg);
test_failure f;
f.executable_path = exe_path;
f.output = res.output;
f.retc = res.retc;
f.signal = res.signal;
f.timed_out = res.timed_out;
return f;
}
}

+ 3
- 2
src/dds/build/plan/exe.hpp View File

@@ -18,8 +18,9 @@ class library_plan;
struct test_failure {
fs::path executable_path;
std::string output;
int retc;
int signal;
int retc{};
int signal{};
bool timed_out = false;
};

/**

+ 10
- 5
src/dds/cli/cmd/pkg_repo_err_handle.cpp View File

@@ -11,9 +11,12 @@
#include <dds/util/result.hpp>

#include <boost/leaf/handle_exception.hpp>
#include <fansi/styled.hpp>
#include <json5/parse_data.hpp>
#include <neo/url.hpp>

using namespace fansi::literals;

int dds::cli::cmd::handle_pkg_repo_remote_errors(std::function<int()> fn) {
return boost::leaf::try_catch(
[&] {
@@ -38,13 +41,16 @@ int dds::cli::cmd::handle_pkg_repo_remote_errors(std::function<int()> fn) {
},
[](const json5::parse_error& e, neo::url bad_url) {
dds_log(error,
"Error parsing JSON downloaded from URL [{}]: {}",
"Error parsing JSON downloaded from URL [.br.red[{}]`]: {}"_styled,
bad_url.to_string(),
e.what());
return 1;
},
[](dds::e_sqlite3_error_exc e, neo::url url) {
dds_log(error, "Error accessing remote database [{}]: {}", url.to_string(), e.message);
dds_log(error,
"Error accessing remote database [.br.red[{}]`]: {}"_styled,
url.to_string(),
e.message);
return 1;
},
[](dds::e_sqlite3_error_exc e) {
@@ -53,7 +59,7 @@ int dds::cli::cmd::handle_pkg_repo_remote_errors(std::function<int()> fn) {
},
[](dds::e_system_error_exc e, dds::network_origin conn) {
dds_log(error,
"Error communicating with [{}://{}:{}]: {}",
"Error communicating with [.br.red[{}://{}:{}]`]: {}"_styled,
conn.protocol,
conn.hostname,
conn.port,
@@ -62,8 +68,7 @@ int dds::cli::cmd::handle_pkg_repo_remote_errors(std::function<int()> fn) {
},
[](matchv<pkg_repo_subcommand::remove>, e_nonesuch missing) {
missing.log_error(
"Cannot delete remote '{}', as no such remote repository is locally registered by "
"that name.");
"Cannot delete remote '.br.red[{}]', as no such remote repository is locally registered by that name."_styled);
write_error_marker("repo-rm-no-such-repo");
return 1;
});

+ 4
- 1
src/dds/error/nonesuch.cpp View File

@@ -2,11 +2,14 @@

#include <dds/util/log.hpp>

#include <fansi/styled.hpp>

using namespace dds;
using namespace fansi::literals;

void e_nonesuch::log_error(std::string_view fmt) const noexcept {
dds_log(error, fmt, given);
if (nearest) {
dds_log(error, " (Did you mean '{}'?)", *nearest);
dds_log(error, " (Did you mean '.br.yellow[{}]'?)"_styled, *nearest);
}
}

+ 6
- 1
src/dds/pkg/remote.cpp View File

@@ -9,6 +9,7 @@
#include <dds/util/log.hpp>
#include <dds/util/result.hpp>

#include <fansi/styled.hpp>
#include <neo/event.hpp>
#include <neo/io/stream/buffers.hpp>
#include <neo/io/stream/file.hpp>
@@ -23,6 +24,7 @@
#include <range/v3/view/transform.hpp>

using namespace dds;
using namespace fansi::literals;
namespace nsql = neo::sqlite3;

namespace {
@@ -77,7 +79,10 @@ void pkg_remote::store(nsql::database_ref db) {
void pkg_remote::update_pkg_db(nsql::database_ref db,
std::optional<std::string_view> etag,
std::optional<std::string_view> db_mtime) {
dds_log(info, "Pulling repository contents for {} [{}]", _name, _base_url.to_string());
dds_log(info,
"Pulling repository contents for .cyan[{}] [{}`]"_styled,
_name,
_base_url.to_string());

auto& pool = http_pool::global_pool();
auto url = _base_url;

+ 35
- 0
src/fansi/style.hpp View File

@@ -0,0 +1,35 @@
#pragma once

#include <string>

namespace fansi {

enum class std_color {
unspecified = -1,
black = 0,
red = 1,
green = 2,
yellow = 3,
blue = 4,
magent = 5,
cyan = 6,
white = 7,
normal = 9,
};

struct text_style {
std_color fg_color = std_color::normal;
std_color bg_color = std_color::normal;

bool bright = false;
bool bold = false;
bool faint = false;
bool italic = false;
bool underline = false;
bool reverse = false;
bool strike = false;
};

bool detect_should_style() noexcept;

} // namespace fansi

+ 173
- 0
src/fansi/styled.cpp View File

@@ -0,0 +1,173 @@
#include "./styled.hpp"

#include "./style.hpp"
#include "./writer.hpp"

#include <magic_enum.hpp>
#include <neo/buffer_algorithm/concat.hpp>
#include <neo/event.hpp>
#include <neo/string_io.hpp>
#include <neo/ufmt.hpp>
#include <neo/utility.hpp>

#include <cctype>
#include <charconv>
#include <map>
#include <vector>

#if NEO_OS_IS_WINDOWS
bool fansi::detect_should_style() noexcept { return false; }
#else
#include <unistd.h>
bool fansi::detect_should_style() noexcept { return ::isatty(STDOUT_FILENO); }
#endif

using namespace fansi;
using namespace neo::buffer_literals;

namespace {

const auto ANSI_CSI = "\x1b["_buf;
// const auto ANSI_RESET = "0m"_buf;
// const auto ANSI_BOLD = "1m"_buf;
// const auto ANSI_RED = "32m"_buf;
// const auto ANSI_GREEN = "32m"_buf;
// const auto ANSI_YELLOW = "33m"_buf;
// const auto ANSI_BLUE = "34m"_buf;
// const auto ANSI_MAGENTA = "35m"_buf;
// const auto ANSI_CYAN = "36m"_buf;
// const auto ANSI_WHITE = "37m"_buf;
// const auto ANSI_GRAY = "90m"_buf;

constexpr text_style default_style{};

struct text_styler {
std::string_view input;
should_style should;
text_writer out{};

std::string_view::iterator s_iter = input.cbegin(), s_place = s_iter, s_stop = input.cend();

bool do_style = (should == should_style::force)
? true
: (should == should_style::never ? false : detect_should_style());

std::vector<text_style> _style_stack = {default_style};

std::string_view slice(std::string_view::iterator it,
std::string_view::iterator st) const noexcept {
return input.substr(it - input.cbegin(), st - it);
}
std::string_view pending() const noexcept { return slice(s_place, s_iter); }
std::string_view remaining() const noexcept { return slice(s_place, input.cend()); }

std::string render() noexcept {
while (s_iter != s_stop) {
if (*s_iter == '`') {
out.write(pending());
++s_iter;
if (s_iter == s_stop) {
neo::emit(ev_warning{"String ends with incomplete escape sequence"});
} else {
out.putc(*s_iter);
}
++s_iter;
s_place = s_iter;
} else if (*s_iter == '.') {
out.write(pending());
s_place = s_iter;
++s_iter;
if (s_iter == s_stop || !std::isalpha(*s_iter)) {
// Just keep going
continue;
}
s_place = s_iter;
_push_style();
} else if (*s_iter == ']' && _style_stack.size() > 1) {
out.write(pending());
s_place = ++s_iter;
_pop_style();
} else {
// Just keep scanning
++s_iter;
}
}
out.write(pending());
return out.take_string();
}

void _push_style() noexcept {
_read_style();
neo_assert(expects,
*s_iter == '[',
"Style sequence should be followed by an opening square brackent");
if (do_style) {
out.put_style(_style_stack.back());
}
s_place = ++s_iter;
}

void _read_style() noexcept {
auto& style = _style_stack.emplace_back(_style_stack.back());
while (s_iter != s_stop) {
if (*s_iter == neo::oper::any_of('[', '.')) {
auto cls = pending();
s_place = s_iter;
_apply_class(style, cls);
if (*s_iter == '[') {
return;
}
s_place = ++s_iter;
}
++s_iter;
}
}

void _apply_class(text_style& style, std::string_view cls) const noexcept {
auto color = magic_enum::enum_cast<std_color>(cls);
if (color) {
style.fg_color = *color;
}
#define CASE(Name) \
else if (cls == #Name) { \
style.Name = true; \
}
CASE(bold)
CASE(faint)
CASE(italic)
CASE(underline)
CASE(reverse)
CASE(strike)
#undef CASE
else if (cls == "br") {
style.bright = true;
}
else {
neo_assert(expects, false, "Invalid text style class in input string", cls);
}
}

void _pop_style() noexcept {
neo_assert(expects,
_style_stack.size() > 1,
"Unbalanced style: Extra closing square brackets");
_style_stack.pop_back();
out.put_style(_style_stack.back());
}
}; // namespace

} // namespace

std::string fansi::stylize(std::string_view str, fansi::should_style should) {
neo_assertion_breadcrumbs("Rendering text style string", str);
return text_styler{str, should}.render();
}

std::string_view detail::cached_rendering(const char* ptr) noexcept {
thread_local std::map<const char*, std::string> cache;
auto found = cache.find(ptr);
if (found == cache.end()) {
found = cache.emplace(ptr, stylize(ptr)).first;
}
return found->second;
}

+ 34
- 0
src/fansi/styled.hpp View File

@@ -0,0 +1,34 @@
#pragma once

#include <cinttypes>
#include <string>
#include <string_view>

namespace fansi {

struct ev_warning {
std::string_view message;
};

enum class should_style {
detect,
force,
never,
};

std::string stylize(std::string_view text, should_style = should_style::detect);

namespace detail {
std::string_view cached_rendering(const char* ptr) noexcept;
}

inline namespace literals {
inline namespace styled_literals {
inline std::string_view operator""_styled(const char* str, std::size_t) {
return detail::cached_rendering(str);
}

} // namespace styled_literals
} // namespace literals

} // namespace fansi

+ 34
- 0
src/fansi/styled.test.cpp View File

@@ -0,0 +1,34 @@
#include "./styled.hpp"

#include <catch2/catch.hpp>

static std::string render(std::string_view fmt) {
return fansi::stylize(fmt, fansi::should_style::force);
}

TEST_CASE("Stylize some text") {
auto test = render("foo bar");
CHECK(test == "foo bar");
test = render("foo. bar.");
CHECK(test == "foo. bar.");
test = render("foo `.eggs");
CHECK(test == "foo .eggs");

test = render("foo `.bar[`]");
CHECK(test == "foo .bar[]");

test = render("foo .bold[bar] baz");
CHECK(test == "foo \x1b[1mbar\x1b[0m baz");

test = render("foo .bold.red[bar] baz");
CHECK(test == "foo \x1b[1;31mbar\x1b[0m baz");

test = render("foo .br.red[bar] baz");
CHECK(test == "foo \x1b[91mbar\x1b[0m baz");

test = render("foo .br.italic[bar] baz");
CHECK(test == "foo \x1b[3mbar\x1b[0m baz");

test = render("foo .red[I am a string with .bold[bold] text inside]");
CHECK(test == "foo \x1b[31mI am a string with \x1b[1mbold\x1b[0;31m text inside\x1b[0m");
}

+ 101
- 0
src/fansi/writer.cpp View File

@@ -0,0 +1,101 @@
#include "./writer.hpp"

#include <neo/buffer_algorithm/concat.hpp>

#include <array>
#include <charconv>

using namespace fansi;
using namespace neo::literals;

namespace {

int code_for_color(std_color col, bool bright) {
return 30 + int(col) + ((bright && col != std_color::normal) ? 60 : 0);
}

} // namespace

void text_writer::put_style(const text_style& new_style) noexcept {
auto& prev_style = _style;
bool unbold = false;
std::string reset_then_enable = "0";
std::string set_toggles;

using neo::dynbuf_concat;

auto append_int = [&](std::string& out, int i) {
std::array<char, 4> valbuf;
auto res = std::to_chars(valbuf.data(), valbuf.data() + sizeof(valbuf), i);
if (!out.empty()) {
out.push_back(';');
}
neo::dynbuf_concat(out, neo::as_buffer(valbuf, res.ptr - valbuf.data()));
};

auto append_toggle = [&](bool my_state, bool prev_state, int on_val) {
int off_val = on_val + 20;
if (!my_state) {
if (prev_state != my_state) {
append_int(set_toggles, off_val);
if (off_val == 21) {
// ! Hack: Terminals disagree on the meaning of 21. ECMA says
// "double-underline", but intuition tells us it would be bold-off, since it is
// SGR Bold [1] plus twenty, as with all other toggles.
unbold = true;
}
}
} else {
append_int(reset_then_enable, on_val);
if (prev_state != my_state) {
append_int(set_toggles, on_val);
}
}
};

append_toggle(new_style.bold, prev_style.bold, 1);
append_toggle(new_style.faint, prev_style.faint, 2);
append_toggle(new_style.italic, prev_style.italic, 3);
append_toggle(new_style.underline, prev_style.underline, 4);
append_toggle(new_style.reverse, prev_style.reverse, 7);
append_toggle(new_style.strike, prev_style.strike, 9);

int fg_int = code_for_color(new_style.fg_color, new_style.bright);
int bg_int = code_for_color(new_style.bg_color, new_style.bright) + 10;
int prev_fg_int = code_for_color(prev_style.fg_color, prev_style.bright);
int prev_bg_int = code_for_color(prev_style.bg_color, prev_style.bright) + 10;

if (new_style.fg_color == std_color::normal) {
// No need to change the foreground color for the reset, but maybe for the toggle
if (fg_int != prev_fg_int) {
append_int(set_toggles, fg_int);
}
} else {
append_int(reset_then_enable, fg_int);
if (fg_int != prev_fg_int) {
append_int(set_toggles, fg_int);
}
}

if (new_style.bg_color == std_color::normal) {
// No need to change the background color for the reset, but maybe for the toggle
if (bg_int != prev_bg_int) {
append_int(set_toggles, bg_int);
}
} else {
append_int(reset_then_enable, bg_int);
if (bg_int != prev_bg_int) {
append_int(set_toggles, bg_int);
}
}

if (set_toggles.empty()) {
// No changes necessary
} else if (unbold || set_toggles.size() > reset_then_enable.size()) {
dynbuf_concat(_buf, "\x1b[", reset_then_enable, "m");
} else {
dynbuf_concat(_buf, "\x1b[", set_toggles, "m");
}

_style = new_style;
}

+ 57
- 0
src/fansi/writer.hpp View File

@@ -0,0 +1,57 @@
#pragma once

#include "./style.hpp"

#include <neo/as_buffer.hpp>
#include <neo/as_dynamic_buffer.hpp>
#include <neo/buffer_algorithm/size.hpp>
#include <neo/buffer_range.hpp>

#include <initializer_list>
#include <string>

namespace fansi {

class text_writer {
std::string _buf;
std::size_t _vis_size = 0;

text_style _style;

template <neo::buffer_range Bufs>
void _write_raw(Bufs&& bufs, std::size_t s) noexcept {
auto out = neo::as_dynamic_buffer(_buf).grow(s);
neo::buffer_copy(out, bufs);
}

template <neo::buffer_range Bufs>
void _write(Bufs&& bufs) noexcept {
auto size = neo::buffer_size(bufs);
_write_raw(bufs, size);
_vis_size += size;
}

public:
template <neo::buffer_range Buf>
void write(Buf&& bufs) noexcept {
_write(bufs);
}

template <neo::as_buffer_convertible B>
requires(!neo::buffer_range<B>) void write(B&& b) noexcept {
auto bufs = {neo::as_buffer(b)};
_write(bufs);
}

void write(std::initializer_list<neo::const_buffer> bufs) noexcept { return _write(bufs); }

void putc(char c) noexcept { write(std::string_view(&c, 1)); }

void put_style(const text_style&) noexcept;

std::string take_string() noexcept { return std::move(_buf); }
std::string_view string() const noexcept { return _buf; }
auto visual_size() const noexcept { return _vis_size; }
};

} // namespace fansi

+ 9
- 0
src/fansi/writer.test.cpp View File

@@ -0,0 +1,9 @@
#include <fansi/writer.hpp>

#include <catch2/catch.hpp>

TEST_CASE("Write a string") {
fansi::text_writer wr;
wr.write("foo");
CHECK(wr.string() == "foo");
}

Loading…
Cancel
Save