Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 77 additions & 69 deletions libwcwidth/src/sgr.c
Original file line number Diff line number Diff line change
Expand Up @@ -108,78 +108,11 @@ set_color(int *dst, int *dst_len, const int *src, int src_len)
}
}

void
wcwidth_sgr_update(wcwidth_sgr_state_t *state, const char *sgr_params, size_t sgr_params_len)
static void
apply_integer_params(wcwidth_sgr_state_t *state, const int *params, int nparams)
{
int stack_params[WCWIDTH_SGR_MAX_PARAMS];
int *params = stack_params;
size_t params_cap = WCWIDTH_SGR_MAX_PARAMS;
int nparams = 0;
const char *p = sgr_params;
const char *end = sgr_params + sgr_params_len;
int i;

/* Empty params is equivalent to "0" (reset): \x1b[m. */
if (sgr_params_len == 0) {
*state = WCWIDTH_SGR_STATE_DEFAULT;
return;
}

/* Phase 1: parse all semicolon-separated segments.
* Colon-separated tuples (ITU T.416 extended colors) are applied
* immediately. Plain integers are collected for phase 2. */
while (p < end) {
const char *seg_end = p;

while (seg_end < end && *seg_end != ';') {
seg_end++;
}
{
size_t seg_len = (size_t) (seg_end - p);

if (seg_len == 0) {
if (!sgr_params_reserve(&params, stack_params, &params_cap, nparams)) {
break;
}
params[nparams++] = 0;
}
else {
int has_colon = 0;
size_t j;

for (j = 0; j < seg_len; j++) {
if (p[j] == ':') {
has_colon = 1;
break;
}
}

if (has_colon) {
int tuple[WCWIDTH_SGR_COLOR_MAX];
int n = parse_colon_tuple(p, seg_len, tuple, WCWIDTH_SGR_COLOR_MAX);
if (n >= 2 && tuple[0] == 38) {
set_color(state->fg, &state->fg_len, tuple, n);
}
else if (n >= 2 && tuple[0] == 48) {
set_color(state->bg, &state->bg_len, tuple, n);
}
}
else {
if (!sgr_params_reserve(&params, stack_params, &params_cap, nparams)) {
break;
}
params[nparams++] = parse_int(p, seg_len);
}
}
}

p = seg_end;
if (p < end && *p == ';') {
p++;
}
}

/* Phase 2: apply integer parameters in order. */
for (i = 0; i < nparams; i++) {
int val = params[i];

Expand Down Expand Up @@ -305,6 +238,81 @@ wcwidth_sgr_update(wcwidth_sgr_state_t *state, const char *sgr_params, size_t sg
}
}
}
}

void
wcwidth_sgr_update(wcwidth_sgr_state_t *state, const char *sgr_params, size_t sgr_params_len)
{
int stack_params[WCWIDTH_SGR_MAX_PARAMS];
int *params = stack_params;
size_t params_cap = WCWIDTH_SGR_MAX_PARAMS;
int nparams = 0;
const char *p = sgr_params;
const char *end = sgr_params + sgr_params_len;

/* Empty params is equivalent to "0" (reset): \x1b[m. */
if (sgr_params_len == 0) {
*state = WCWIDTH_SGR_STATE_DEFAULT;
return;
}

/* Collect runs of integer parameters, applying each run before the next
* colon-separated color so resets and colors retain their original order. */
while (p < end) {
const char *seg_end = p;

while (seg_end < end && *seg_end != ';') {
seg_end++;
}
{
size_t seg_len = (size_t) (seg_end - p);

if (seg_len == 0) {
if (!sgr_params_reserve(&params, stack_params, &params_cap, nparams)) {
break;
}
params[nparams++] = 0;
}
else {
int has_colon = 0;
size_t j;

for (j = 0; j < seg_len; j++) {
if (p[j] == ':') {
has_colon = 1;
break;
}
}

if (has_colon) {
int tuple[WCWIDTH_SGR_COLOR_MAX];
int n = parse_colon_tuple(p, seg_len, tuple, WCWIDTH_SGR_COLOR_MAX);

apply_integer_params(state, params, nparams);
nparams = 0;
if (n >= 2 && tuple[0] == 38) {
set_color(state->fg, &state->fg_len, tuple, n);
}
else if (n >= 2 && tuple[0] == 48) {
set_color(state->bg, &state->bg_len, tuple, n);
}
}
else {
if (!sgr_params_reserve(&params, stack_params, &params_cap, nparams)) {
break;
}
params[nparams++] = parse_int(p, seg_len);
}
}
}

p = seg_end;
if (p < end && *p == ';') {
p++;
}
}

apply_integer_params(state, params, nparams);

if (params != stack_params) {
free(params);
Expand Down
28 changes: 28 additions & 0 deletions libwcwidth/tests/test_sgr.c
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,33 @@ TEST(attribute_and_color_codes)
}
}

TEST(mixed_color_parameter_order)
{
static const char *const inputs[] = {
"0;38:2::255:0:0", "0;48:2::0:0:255", "31;38:5:196", "39;38:5:196",
"38;5;196;38:5:46", "38:5:196;38;5;46", "38:5:196;0",
};
static const char *const expected[] = {
"\x1b[38:2:0:255:0:0m",
"\x1b[48:2:0:0:0:255m",
"\x1b[38;5;196m",
"\x1b[38;5;196m",
"\x1b[38;5;46m",
"\x1b[38;5;46m",
"",
};
size_t i;

for (i = 0; i < sizeof(inputs) / sizeof(inputs[0]); i++) {
wcwidth_sgr_state_t s = WCWIDTH_SGR_STATE_DEFAULT;
char buf[64];

wcwidth_sgr_update(&s, inputs[i], strlen(inputs[i]));
wcwidth_sgr_to_escape(&s, buf, sizeof(buf));
ASSERT_STREQ(expected[i], buf);
}
}

int
main(void)
{
Expand All @@ -221,5 +248,6 @@ main(void)
RUN_TEST(to_escape_colon_form);
RUN_TEST(propagate_basic);
RUN_TEST(attribute_and_color_codes);
RUN_TEST(mixed_color_parameter_order);
return test_summary();
}
22 changes: 22 additions & 0 deletions tests/test_sgr_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import pytest

# local
import wcwidth
from wcwidth import clip, wrap
from wcwidth.sgr_state import (_SGR_STATE_DEFAULT,
_SGRState,
Expand Down Expand Up @@ -171,6 +172,27 @@ def test_propagate_sgr_colon_format_color_is_restored():
(38, 2, 0, 255, 0, 0))


@pytest.mark.parametrize('params,expected', [
('0;38:2::255:0:0', '\x1b[38:2:0:255:0:0m'),
('0;48:2::0:0:255', '\x1b[48:2:0:0:0:255m'),
('31;38:5:196', '\x1b[38;5;196m'),
('39;38:5:196', '\x1b[38;5;196m'),
('38;5;196;38:5:46', '\x1b[38;5;46m'),
('38:5:196;38;5;46', '\x1b[38;5;46m'),
('38:5:196;0', ''),
])
@pytest.mark.parametrize('operation', ['propagate_sgr', 'clip'])
def test_mixed_sgr_parameters_keep_order(params, expected, operation):
"""Apply resets and colors in order when semicolon and colon forms mix."""
opening = f'\x1b[{params}m'
reset = '\x1b[0m' if expected else ''
if operation == 'propagate_sgr':
result = wcwidth.propagate_sgr([opening + 'red', 'text'])[1]
else:
result = clip(opening + 'red text', 4, 8)
assert result == expected + 'text' + reset


def test_sgr_state_color_override():
"""Newer color replaces older regardless of format."""
state = _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[38;5;208m')
Expand Down