From 28b466a1c6ea5ff5f2a68992d6e865f3b671a7b6 Mon Sep 17 00:00:00 2001 From: "Dennis Lambe Jr." Date: Fri, 7 Nov 2025 13:45:49 -0500 Subject: [PATCH 1/4] Respond to OSC 10 (fg color) and OSC 11 (bg color) queries Add do_osc_internal() for handling certain OSC commands within libtsm instead of delegating them to osc_cb. Fix #53 --- src/tsm/tsm-vte.c | 48 ++++++++++++++++++++++++++++++++++---- test/test_vte.c | 59 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+), 4 deletions(-) diff --git a/src/tsm/tsm-vte.c b/src/tsm/tsm-vte.c index cdbdc5e..6874238 100644 --- a/src/tsm/tsm-vte.c +++ b/src/tsm/tsm-vte.c @@ -2059,12 +2059,52 @@ static void do_osc_collect(struct tsm_vte *vte, uint32_t val) { vte->osc_len += len; } -static void do_osc_end(struct tsm_vte *vte) { - if (!vte->osc_cb) { - return; +static void vte_write_xcolor(struct tsm_vte *vte, const char *code, + const char *end_seq, + uint8_t r, uint8_t g, uint8_t b) +{ + char buf[32]; + snprintf(buf, sizeof(buf), "\e]%s;rgb:%02x%02x/%02x%02x/%02x%02x%s", + code, r, r, g, g, b, b, end_seq); + vte_write(vte, buf, strlen(buf)); +} + +static bool do_osc_internal(struct tsm_vte *vte, const char *end_seq) +{ + if (!strncmp(vte->osc_arg, "10;?", 4)) { + vte_write_xcolor(vte, "10", end_seq, + vte->def_attr.fr, vte->def_attr.fg, + vte->def_attr.fb); + return true; + } + if (!strncmp(vte->osc_arg, "11;?", 4)) { + vte_write_xcolor(vte, "11", end_seq, + vte->def_attr.br, vte->def_attr.bg, + vte->def_attr.bb); + return true; } + return false; +} +/* XTerm OSC responses use the same sequence terminator used in the query */ +static const char *osc_end_seq(uint32_t end) +{ + if (end == 0x07) { + return "\x07"; + } + return "\x1b\x5c"; +} + +static void do_osc_end(struct tsm_vte *vte, uint32_t val) { + const char *end_seq = osc_end_seq(val); vte->osc_arg[vte->osc_len] = 0; + + if (do_osc_internal(vte, end_seq)) { + return; + } + if (!vte->osc_cb) { + return; + } vte->osc_cb(vte, vte->osc_arg, vte->osc_len, vte->osc_data); } @@ -2115,7 +2155,7 @@ static void do_action(struct tsm_vte *vte, uint32_t data, int action) do_osc_collect(vte, data); break; case ACTION_OSC_END: - do_osc_end(vte); + do_osc_end(vte, data); break; default: llog_warning(vte, "invalid action %d", action); diff --git a/test/test_vte.c b/test/test_vte.c index a109f13..bdff98a 100644 --- a/test/test_vte.c +++ b/test/test_vte.c @@ -164,8 +164,12 @@ START_TEST(test_vte_custom_palette) } END_TEST +static bool write_cb_called = false; + static void checking_write_cb(struct tsm_vte *vte, const char *u8, size_t len, void *data) { + write_cb_called = true; + ck_assert_ptr_ne(vte, NULL); ck_assert_ptr_ne(u8, NULL); ck_assert_uint_gt(len, 0); @@ -173,6 +177,60 @@ static void checking_write_cb(struct tsm_vte *vte, const char *u8, size_t len, v ck_assert_mem_eq(u8, data, len); } +static void checked_vte_input(struct tsm_vte *vte, const char *u8, size_t len) +{ + write_cb_called = false; + tsm_vte_input(vte, u8, len); + ck_assert(write_cb_called); +} + +START_TEST(test_vte_osc_query) +{ + struct tsm_screen *screen; + struct tsm_vte *vte; + int r; + char expected_output[32]; + const char *input; + + r = tsm_screen_new(&screen, log_cb, NULL); + ck_assert_int_eq(r, 0); + + r = tsm_vte_new(&vte, screen, checking_write_cb, expected_output, NULL, NULL); + ck_assert_int_eq(r, 0); + + r = tsm_vte_set_custom_palette(vte, test_palette); + ck_assert_int_eq(r, 0); + + r = tsm_vte_set_palette(vte, "custom"); + ck_assert_int_eq(r, 0); + + // query foreground, end_seq = BEL + input = "\e]10;?\x07"; + strcpy(expected_output, "\e]10;rgb:1010/2222/3434\x07"); + checked_vte_input(vte, input, strlen(input)); + + // query foreground, end_seq = ST + input = "\e]10;?\e\\"; + strcpy(expected_output, "\e]10;rgb:1010/2222/3434\e\\"); + checked_vte_input(vte, input, strlen(input)); + + // ignore additional parameters after foreground query + input = "\e]10;?;11;?\x07"; + strcpy(expected_output, "\e]10;rgb:1010/2222/3434\x07"); + checked_vte_input(vte, input, strlen(input)); + + // query background + input = "\e]11;?\x07"; + strcpy(expected_output, "\e]11;rgb:1111/2323/3535\x07"); + checked_vte_input(vte, input, strlen(input)); + + // ignore additional parameters after background query + input = "\e]11;?;12;?\x07"; + strcpy(expected_output, "\e]11;rgb:1111/2323/3535\x07"); + checked_vte_input(vte, input, strlen(input)); +} +END_TEST + START_TEST(test_vte_backspace_key) { struct tsm_screen *screen; @@ -337,6 +395,7 @@ TEST_DEFINE_CASE(misc) TEST(test_vte_init) TEST(test_vte_null) TEST(test_vte_custom_palette) + TEST(test_vte_osc_query) TEST(test_vte_backspace_key) TEST(test_vte_get_flags) TEST(test_vte_decrqm_no_reset) From ce10c471601f1735d6cd8cf17fc23cd239b7f4c6 Mon Sep 17 00:00:00 2001 From: "Dennis Lambe Jr." Date: Sat, 8 Nov 2025 00:07:36 -0500 Subject: [PATCH 2/4] abstract 256-color code lookup out of csi_attribute This is in preparation for OSC 4 ? query support. --- src/tsm/tsm-vte.c | 51 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 38 insertions(+), 13 deletions(-) diff --git a/src/tsm/tsm-vte.c b/src/tsm/tsm-vte.c index 6874238..3f56332 100644 --- a/src/tsm/tsm-vte.c +++ b/src/tsm/tsm-vte.c @@ -1220,9 +1220,45 @@ static void do_esc(struct tsm_vte *vte, uint32_t data) } } -static void csi_attribute(struct tsm_vte *vte) +static void palette_rgb(struct tsm_vte *vte, int code, uint8_t *cr, + uint8_t *cg, uint8_t *cb) +{ + *cr = vte->palette[code][0]; + *cg = vte->palette[code][1]; + *cb = vte->palette[code][2]; +} + +static void cube_rgb(int code, uint8_t *cr, uint8_t *cg, uint8_t *cb) { static const uint8_t bval[6] = { 0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff }; + code -= 16; + *cb = bval[code % 6]; + code /= 6; + *cg = bval[code % 6]; + code /= 6; + *cr = bval[code % 6]; +} + +static void greyscale_rgb(int code, uint8_t *cr, uint8_t *cg, uint8_t *cb) +{ + uint8_t lightness = (code - 232) * 10 + 8; + *cr = *cg = *cb = lightness; +} + +static void lookup_color(struct tsm_vte *vte, int color, uint8_t *cr, + uint8_t *cg, uint8_t *cb) +{ + if (color < 16) { + palette_rgb(vte, color, cr, cg, cb); + } else if (color < 232) { + cube_rgb(color, cr, cg, cb); + } else { + greyscale_rgb(color, cr, cg, cb); + } +} + +static void csi_attribute(struct tsm_vte *vte) +{ unsigned int i, code, val; uint8_t cr, cg, cb; @@ -1390,19 +1426,8 @@ static void csi_attribute(struct tsm_vte *vte) if (code < 16) { //no change cb = cg = cr = 0; - } else if (code < 232) { - code -= 16; - cb = bval[code % 6]; - code /= 6; - cg = bval[code % 6]; - code /= 6; - cr = bval[code % 6]; - code = -1; } else { - code = (code - 232) * 10 + 8; - cr = code; - cg = code; - cb = code; + lookup_color(vte, code, &cr, &cg, &cb); code = -1; } i += 2; From 1ddb70e707c5813f129e84141867e95f5c9e1d34 Mon Sep 17 00:00:00 2001 From: "Dennis Lambe Jr." Date: Sat, 8 Nov 2025 00:10:09 -0500 Subject: [PATCH 3/4] Respond to OSC 4 queries --- src/tsm/tsm-vte.c | 42 ++++++++++++++++++++++++++++- test/test_vte.c | 68 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+), 1 deletion(-) diff --git a/src/tsm/tsm-vte.c b/src/tsm/tsm-vte.c index 3f56332..947e44e 100644 --- a/src/tsm/tsm-vte.c +++ b/src/tsm/tsm-vte.c @@ -2084,7 +2084,7 @@ static void do_osc_collect(struct tsm_vte *vte, uint32_t val) { vte->osc_len += len; } -static void vte_write_xcolor(struct tsm_vte *vte, const char *code, +static void vte_write_xcolor(struct tsm_vte *vte, char *code, const char *end_seq, uint8_t r, uint8_t g, uint8_t b) { @@ -2094,8 +2094,48 @@ static void vte_write_xcolor(struct tsm_vte *vte, const char *code, vte_write(vte, buf, strlen(buf)); } +static void do_osc_4(struct tsm_vte *vte, const char *data, const char *end_seq) +{ + char buf[32]; + uint8_t cr, cg, cb; + const char *c = data; + + while (*c) { + // parse color code + unsigned int code = 0; + while (*c >= '0' && *c <= '9') { + code = code * 10 + (*c++ - '0'); + } + if (*c++ != ';') { + break; + } + + // parse value field + if (*c == '?') { + c++; + // handle rgb value query + snprintf(buf, sizeof(buf), "4;%u", code); + lookup_color(vte, code, &cr, &cg, &cb); + vte_write_xcolor(vte, buf, end_seq, cr, cg, cb); + } else { + // parsing of new rgb value to assign to code would go + // here. For now, skip past field contents. + while (*c && *c != ';') { + c++; + } + } + if (*c++ != ';') { + break; + } + } +} + static bool do_osc_internal(struct tsm_vte *vte, const char *end_seq) { + if (!strncmp(vte->osc_arg, "4;", 2)) { + do_osc_4(vte, vte->osc_arg + 2, end_seq); + return true; + } if (!strncmp(vte->osc_arg, "10;?", 4)) { vte_write_xcolor(vte, "10", end_seq, vte->def_attr.fr, vte->def_attr.fg, diff --git a/test/test_vte.c b/test/test_vte.c index bdff98a..9aae9f6 100644 --- a/test/test_vte.c +++ b/test/test_vte.c @@ -231,6 +231,73 @@ START_TEST(test_vte_osc_query) } END_TEST +unsigned char write_buffer[512]; +unsigned char *write_buffer_p = write_buffer; + +static void storing_write_cb(struct tsm_vte *vte, const char *u8, size_t len, void *data) +{ + ck_assert_ptr_ne(vte, NULL); + ck_assert_ptr_ne(u8, NULL); + + memcpy(write_buffer_p, u8, len); + write_buffer_p[len] = '\0'; + write_buffer_p += len; +} + +static void storing_write_cb_reset(void) +{ + memset(write_buffer, 0, sizeof(write_buffer)); + write_buffer_p = write_buffer; +} + +START_TEST(test_vte_osc4) +{ + struct tsm_screen *screen; + struct tsm_vte *vte; + int r; + const char *input; + + r = tsm_screen_new(&screen, log_cb, NULL); + ck_assert_int_eq(r, 0); + + r = tsm_vte_new(&vte, screen, storing_write_cb, NULL, NULL, NULL); + ck_assert_int_eq(r, 0); + + r = tsm_vte_set_custom_palette(vte, test_palette); + ck_assert_int_eq(r, 0); + + r = tsm_vte_set_palette(vte, "custom"); + ck_assert_int_eq(r, 0); + + // query palette entries + storing_write_cb_reset(); + input = "\e]4;1;?;13;?;3;?\x07"; + const char *expected = + "\e]4;1;rgb:0101/1313/2525\x07" + "\e]4;13;rgb:0d0d/1f1f/3131\x07" + "\e]4;3;rgb:0303/1515/2727\x07"; + tsm_vte_input(vte, input, strlen(input)); + ck_assert_mem_eq(write_buffer, expected, strlen(expected)); + ck_assert_int_eq(write_buffer_p - write_buffer, strlen(expected)); + + // query cube & grayscale entries + storing_write_cb_reset(); + input = "\e]4;110;?;254;?;\x07"; + expected = + "\e]4;110;rgb:8787/afaf/d7d7\x07" + "\e]4;254;rgb:e4e4/e4e4/e4e4\x07"; + tsm_vte_input(vte, input, strlen(input)); + ck_assert_mem_eq(write_buffer, expected, strlen(expected)); + + // ignore color change requests, incomplete messages + storing_write_cb_reset(); + input = "\e]4;1;rgb:1111/2222/3333;2\x07"; + tsm_vte_input(vte, input, strlen(input)); + ck_assert_ptr_eq(write_buffer, write_buffer_p); + storing_write_cb_reset(); +} +END_TEST + START_TEST(test_vte_backspace_key) { struct tsm_screen *screen; @@ -396,6 +463,7 @@ TEST_DEFINE_CASE(misc) TEST(test_vte_null) TEST(test_vte_custom_palette) TEST(test_vte_osc_query) + TEST(test_vte_osc4) TEST(test_vte_backspace_key) TEST(test_vte_get_flags) TEST(test_vte_decrqm_no_reset) From 4dd2f83baf8722c0d4bf0c5f08cf1b7fe541bc27 Mon Sep 17 00:00:00 2001 From: "Dennis Lambe Jr." Date: Sun, 9 Nov 2025 13:53:29 -0500 Subject: [PATCH 4/4] comment explaining OSC 4/10/11 query vs. set limitation --- src/tsm/tsm-vte.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/tsm/tsm-vte.c b/src/tsm/tsm-vte.c index 947e44e..44bfd4a 100644 --- a/src/tsm/tsm-vte.c +++ b/src/tsm/tsm-vte.c @@ -2132,6 +2132,11 @@ static void do_osc_4(struct tsm_vte *vte, const char *data, const char *end_seq) static bool do_osc_internal(struct tsm_vte *vte, const char *end_seq) { + // OSC 4, 10, and 11 query or change the RGB values of color numbers in + // the foreground/background (SGR 38 & 48), 3-bit (SGR 30-37 & 40-47), + // 4-bit (SGR 90-97 & 100-107), and 256-color (SGR 38;5 & 48;5) indexed + // color tables. We don't support changing the tables, but we do support + // querying them. if (!strncmp(vte->osc_arg, "4;", 2)) { do_osc_4(vte, vte->osc_arg + 2, end_seq); return true;