#include <dds/util/output.hpp> | #include <dds/util/output.hpp> | ||||
#include <dds/util/time.hpp> | #include <dds/util/time.hpp> | ||||
#include <fansi/styled.hpp> | |||||
#include <array> | #include <array> | ||||
#include <set> | #include <set> | ||||
using namespace dds; | using namespace dds; | ||||
using namespace fansi::literals; | |||||
namespace { | namespace { | ||||
}; | }; | ||||
void log_failure(const test_failure& fail) { | 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) { | if (fail.signal) { | ||||
dds_log(error, "Test execution received signal {}", fail.signal); | dds_log(error, "Test execution received signal {}", fail.signal); | ||||
} | } | ||||
if (trim_view(fail.output).empty()) { | if (trim_view(fail.output).empty()) { | ||||
dds_log(error, "(Test executable produced no output"); | |||||
dds_log(error, "(Test executable produced no output)"); | |||||
} else { | } else { | ||||
dds_log(error, "Test output:\n{}[dds - test output end]", fail.output); | dds_log(error, "Test output:\n{}[dds - test output end]", fail.output); | ||||
} | } |
#include <dds/util/log.hpp> | #include <dds/util/log.hpp> | ||||
#include <dds/util/time.hpp> | #include <dds/util/time.hpp> | ||||
#include <fansi/styled.hpp> | |||||
#include <range/v3/range/conversion.hpp> | #include <range/v3/range/conversion.hpp> | ||||
#include <range/v3/view/transform.hpp> | #include <range/v3/view/transform.hpp> | ||||
using namespace dds; | using namespace dds; | ||||
using namespace fansi::literals; | |||||
fs::path create_archive_plan::calc_archive_file_path(const toolchain& tc) const noexcept { | fs::path create_archive_plan::calc_archive_file_path(const toolchain& tc) const noexcept { | ||||
return _subdir / fmt::format("{}{}{}", "lib", _name, tc.archive_suffix()); | return _subdir / fmt::format("{}{}{}", "lib", _name, tc.archive_suffix()); | ||||
"Creating static library archive [{}] failed for '{}'", | "Creating static library archive [{}] failed for '{}'", | ||||
out_relpath, | out_relpath, | ||||
_qual_name); | _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< | throw_external_error< | ||||
errc::archive_failure>("Creating static library archive [{}] failed for '{}'", | errc::archive_failure>("Creating static library archive [{}] failed for '{}'", | ||||
out_relpath, | out_relpath, |
#include <dds/util/string.hpp> | #include <dds/util/string.hpp> | ||||
#include <dds/util/time.hpp> | #include <dds/util/time.hpp> | ||||
#include <fansi/styled.hpp> | |||||
#include <neo/assert.hpp> | #include <neo/assert.hpp> | ||||
#include <range/v3/range/conversion.hpp> | #include <range/v3/range/conversion.hpp> | ||||
#include <range/v3/view/filter.hpp> | #include <range/v3/view/filter.hpp> | ||||
using namespace dds; | using namespace dds; | ||||
using namespace ranges; | using namespace ranges; | ||||
using namespace fansi::literals; | |||||
namespace { | namespace { | ||||
// Generate a log message to display to the user | // Generate a log message to display to the user | ||||
auto source_path = cf.plan.source_path(); | auto source_path = cf.plan.source_path(); | ||||
auto msg = fmt::format("[{}] Compile: {}", | |||||
auto msg = fmt::format("[{}] Compile: .br.cyan[{}]"_styled, | |||||
cf.plan.qualifier(), | cf.plan.qualifier(), | ||||
fs::relative(source_path, cf.plan.source().basis_path).string()); | fs::relative(source_path, cf.plan.source().basis_path).string()); | ||||
if (!compiled_okay) { | if (!compiled_okay) { | ||||
dds_log(error, "Compilation failed: {}", source_path.string()); | dds_log(error, "Compilation failed: {}", source_path.string()); | ||||
dds_log(error, | dds_log(error, | ||||
"Subcommand FAILED [Exitted {}]: {}\n{}", | |||||
"Subcommand .bold.red[FAILED] [Exited {}]: .bold.yellow[{}]\n{}"_styled, | |||||
compile_retc, | compile_retc, | ||||
quote_command(cf.cmd_info.command), | quote_command(cf.cmd_info.command), | ||||
compiler_output); | compiler_output); |
#include <dds/util/log.hpp> | #include <dds/util/log.hpp> | ||||
#include <dds/util/time.hpp> | #include <dds/util/time.hpp> | ||||
#include <fansi/styled.hpp> | |||||
#include <algorithm> | #include <algorithm> | ||||
#include <chrono> | #include <chrono> | ||||
using namespace dds; | using namespace dds; | ||||
using namespace fansi::literals; | |||||
fs::path link_executable_plan::calc_executable_path(build_env_ref env) const noexcept { | 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()); | return env.output_root / _out_subdir / (_name + env.toolchain.executable_suffix()); | ||||
std::optional<test_failure> link_executable_plan::run_test(build_env_ref env) const { | std::optional<test_failure> link_executable_plan::run_test(build_env_ref env) const { | ||||
auto exe_path = calc_executable_path(env); | 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); | dds_log(info, msg); | ||||
using namespace std::chrono_literals; | using namespace std::chrono_literals; | ||||
auto&& [dur, res] = timed<std::chrono::microseconds>( | auto&& [dur, res] = timed<std::chrono::microseconds>( | ||||
[&] { return run_proc({.command = {exe_path.string()}, .timeout = 10s}); }); | [&] { return run_proc({.command = {exe_path.string()}, .timeout = 10s}); }); | ||||
if (res.okay()) { | 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; | return std::nullopt; | ||||
} else { | } else { | ||||
auto exit_msg = fmt::format(res.signal ? "signalled {}" : "exited {}", | auto exit_msg = fmt::format(res.signal ? "signalled {}" : "exited {}", | ||||
res.signal ? res.signal : res.retc); | 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); | dds_log(error, "{} - {} - {:>9L}μs [{}]", msg, fail_str, dur.count(), exit_msg); | ||||
test_failure f; | test_failure f; | ||||
f.executable_path = exe_path; | f.executable_path = exe_path; | ||||
f.output = res.output; | f.output = res.output; | ||||
f.retc = res.retc; | f.retc = res.retc; | ||||
f.signal = res.signal; | f.signal = res.signal; | ||||
f.timed_out = res.timed_out; | |||||
return f; | return f; | ||||
} | } | ||||
} | } |
struct test_failure { | struct test_failure { | ||||
fs::path executable_path; | fs::path executable_path; | ||||
std::string output; | std::string output; | ||||
int retc; | |||||
int signal; | |||||
int retc{}; | |||||
int signal{}; | |||||
bool timed_out = false; | |||||
}; | }; | ||||
/** | /** |
#include <dds/util/result.hpp> | #include <dds/util/result.hpp> | ||||
#include <boost/leaf/handle_exception.hpp> | #include <boost/leaf/handle_exception.hpp> | ||||
#include <fansi/styled.hpp> | |||||
#include <json5/parse_data.hpp> | #include <json5/parse_data.hpp> | ||||
#include <neo/url.hpp> | #include <neo/url.hpp> | ||||
using namespace fansi::literals; | |||||
int dds::cli::cmd::handle_pkg_repo_remote_errors(std::function<int()> fn) { | int dds::cli::cmd::handle_pkg_repo_remote_errors(std::function<int()> fn) { | ||||
return boost::leaf::try_catch( | return boost::leaf::try_catch( | ||||
[&] { | [&] { | ||||
}, | }, | ||||
[](const json5::parse_error& e, neo::url bad_url) { | [](const json5::parse_error& e, neo::url bad_url) { | ||||
dds_log(error, | dds_log(error, | ||||
"Error parsing JSON downloaded from URL [{}]: {}", | |||||
"Error parsing JSON downloaded from URL [.br.red[{}]`]: {}"_styled, | |||||
bad_url.to_string(), | bad_url.to_string(), | ||||
e.what()); | e.what()); | ||||
return 1; | return 1; | ||||
}, | }, | ||||
[](dds::e_sqlite3_error_exc e, neo::url url) { | [](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; | return 1; | ||||
}, | }, | ||||
[](dds::e_sqlite3_error_exc e) { | [](dds::e_sqlite3_error_exc e) { | ||||
}, | }, | ||||
[](dds::e_system_error_exc e, dds::network_origin conn) { | [](dds::e_system_error_exc e, dds::network_origin conn) { | ||||
dds_log(error, | dds_log(error, | ||||
"Error communicating with [{}://{}:{}]: {}", | |||||
"Error communicating with [.br.red[{}://{}:{}]`]: {}"_styled, | |||||
conn.protocol, | conn.protocol, | ||||
conn.hostname, | conn.hostname, | ||||
conn.port, | conn.port, | ||||
}, | }, | ||||
[](matchv<pkg_repo_subcommand::remove>, e_nonesuch missing) { | [](matchv<pkg_repo_subcommand::remove>, e_nonesuch missing) { | ||||
missing.log_error( | 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"); | write_error_marker("repo-rm-no-such-repo"); | ||||
return 1; | return 1; | ||||
}); | }); |
#include <dds/util/log.hpp> | #include <dds/util/log.hpp> | ||||
#include <fansi/styled.hpp> | |||||
using namespace dds; | using namespace dds; | ||||
using namespace fansi::literals; | |||||
void e_nonesuch::log_error(std::string_view fmt) const noexcept { | void e_nonesuch::log_error(std::string_view fmt) const noexcept { | ||||
dds_log(error, fmt, given); | dds_log(error, fmt, given); | ||||
if (nearest) { | if (nearest) { | ||||
dds_log(error, " (Did you mean '{}'?)", *nearest); | |||||
dds_log(error, " (Did you mean '.br.yellow[{}]'?)"_styled, *nearest); | |||||
} | } | ||||
} | } |
#include <dds/util/log.hpp> | #include <dds/util/log.hpp> | ||||
#include <dds/util/result.hpp> | #include <dds/util/result.hpp> | ||||
#include <fansi/styled.hpp> | |||||
#include <neo/event.hpp> | #include <neo/event.hpp> | ||||
#include <neo/io/stream/buffers.hpp> | #include <neo/io/stream/buffers.hpp> | ||||
#include <neo/io/stream/file.hpp> | #include <neo/io/stream/file.hpp> | ||||
#include <range/v3/view/transform.hpp> | #include <range/v3/view/transform.hpp> | ||||
using namespace dds; | using namespace dds; | ||||
using namespace fansi::literals; | |||||
namespace nsql = neo::sqlite3; | namespace nsql = neo::sqlite3; | ||||
namespace { | namespace { | ||||
void pkg_remote::update_pkg_db(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> etag, | ||||
std::optional<std::string_view> db_mtime) { | 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& pool = http_pool::global_pool(); | ||||
auto url = _base_url; | auto url = _base_url; |
#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 |
#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; | |||||
} |
#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 |
#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"); | |||||
} |
#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; | |||||
} |
#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 |
#include <fansi/writer.hpp> | |||||
#include <catch2/catch.hpp> | |||||
TEST_CASE("Write a string") { | |||||
fansi::text_writer wr; | |||||
wr.write("foo"); | |||||
CHECK(wr.string() == "foo"); | |||||
} |