13const std::string ansi_reset =
"\033[0m";
23std::string consume_escape(
const std::string &line, std::size_t &pos)
25 const std::size_t
start = pos;
28 while (pos < line.size() &&
29 (
static_cast<unsigned char>(line[pos]) < 0x40 ||
static_cast<unsigned char>(line[pos]) > 0x7E)) {
32 if (pos < line.size()) {
39std::string consume_codepoint(
const std::string &line, std::size_t &pos)
41 const std::size_t
start = pos;
42 const auto lead =
static_cast<unsigned char>(line[pos]);
44 if ((lead & 0x80U) != 0) {
45 if ((lead & 0xE0U) == 0xC0U) {
48 else if ((lead & 0xF0U) == 0xE0U) {
51 else if ((lead & 0xF8U) == 0xF0U) {
55 len = std::min(len, line.size() - pos);
57 return line.substr(
start, len);
61std::vector<Cell> tokenize(
const std::string &line, std::string &trailing_out)
63 std::vector<Cell> cells;
64 std::string pending_prefix;
66 while (pos < line.size()) {
67 if (
static_cast<unsigned char>(line[pos]) == 0x1B && pos + 1 < line.size() && line[pos + 1] ==
'[') {
68 pending_prefix += consume_escape(line, pos);
71 std::string glyph = consume_codepoint(line, pos);
72 const bool is_space = glyph ==
" ";
73 cells.push_back(Cell{std::move(pending_prefix), std::move(glyph), is_space});
74 pending_prefix.clear();
77 trailing_out = pending_prefix;
82void apply_sgr(std::string &active,
const std::string &prefix)
85 while (pos < prefix.size()) {
86 const std::size_t
start = pos;
87 std::string seq = consume_escape(prefix, pos);
88 if (seq == ansi_reset || seq ==
"\033[m") {
101std::vector<std::string>
wrap_ansi_line(
const std::string &line,
const std::size_t width)
107 std::string trailing;
108 const std::vector<Cell> cells = tokenize(line, trailing);
116 std::vector<std::string> active_entering(cells.size() + 1);
117 for (std::size_t i = 0; i < cells.size(); ++i) {
118 active_entering[i + 1] = active_entering[i];
119 apply_sgr(active_entering[i + 1], cells[i].prefix);
123 std::vector<std::pair<std::size_t, std::size_t>> ranges;
125 while (i < cells.size()) {
126 const std::size_t line_start = i;
127 std::size_t visible = 0;
128 std::size_t last_space = cells.size();
130 while (j < cells.size() && visible < width) {
131 if (cells[j].is_space) {
137 if (j == cells.size()) {
138 ranges.emplace_back(line_start, cells.size());
141 if (cells[j].is_space) {
143 ranges.emplace_back(line_start, j);
144 while (j < cells.size() && cells[j].is_space) {
149 else if (last_space != cells.size() && last_space > line_start) {
151 ranges.emplace_back(line_start, last_space);
156 ranges.emplace_back(line_start, j);
162 std::vector<std::string> result;
163 result.reserve(ranges.size());
164 for (std::size_t r = 0; r < ranges.size(); ++r) {
165 const auto [
start,
end] = ranges[r];
166 std::string rendered = active_entering[
start];
167 for (std::size_t k =
start; k <
end; ++k) {
168 rendered += cells[k].prefix;
169 rendered += cells[k].glyph;
171 const bool is_last = r + 1 == ranges.size();
173 rendered += trailing;
175 std::string active_at_end = active_entering[
end];
177 apply_sgr(active_at_end, trailing);
179 if (!active_at_end.empty()) {
180 rendered += ansi_reset;
182 result.push_back(std::move(rendered));
std::vector< std::string > wrap_ansi_line(const std::string &line, std::size_t width)
Word-wraps a single logical line to a visible column width, preserving ANSI styling.