diff options
| -rw-r--r-- | README.md | 3 | ||||
| -rw-r--r-- | include/term/term.h | 7 | ||||
| -rw-r--r-- | src/escape.c | 4 | ||||
| -rw-r--r-- | src/internal.h | 2 | ||||
| -rw-r--r-- | src/osc/misc.c | 2 | ||||
| -rw-r--r-- | src/screen.c | 17 | ||||
| -rw-r--r-- | tests/dcs_test.c | 53 | ||||
| -rw-r--r-- | tests/model.c | 6 | ||||
| -rw-r--r-- | tests/model.h | 5 | ||||
| -rw-r--r-- | tests/model_row_test.c | 2 | ||||
| -rw-r--r-- | tests/model_test.c | 2 | ||||
| -rw-r--r-- | tests/osc_test.c | 32 | ||||
| -rw-r--r-- | tests/term_test.c | 27 |
13 files changed, 147 insertions, 15 deletions
@@ -65,7 +65,8 @@ no flags to get them: A host wires the terminal in four places: 1. Create it with `term_new()`. Set `term_options.callbacks.reply` to - send replies to the program. + send replies to the program. Set `term_options.name` to the name + programs read with XTVERSION and XTGETTCAP. It defaults to `term`. 2. Feed program output with `term_feed()`. 3. Translate key and mouse events into `struct term_key_event` and `struct term_mouse_event`. Encode them with `term_input_encode_key()` diff --git a/include/term/term.h b/include/term/term.h index 8a1e024..9d5a882 100644 --- a/include/term/term.h +++ b/include/term/term.h @@ -161,6 +161,13 @@ struct term_options { struct term_theme theme; enum term_cursor_style cursor_style; bool cursor_steady; /* false: the cursor blinks */ + /* + * The terminal's name. Programs read it with XTVERSION and XTGETTCAP. + * It is also the title of a notification that has none. NULL or "" + * selects "term". Otherwise 1 to 63 printable ASCII bytes, or term_new() + * returns NULL. term_new() copies the name. + */ + const char *name; struct term_callbacks callbacks; /* Must be zero. term_new() returns NULL if any word is nonzero, so a * later version can give a word a meaning. */ diff --git a/src/escape.c b/src/escape.c index da0449a..dceb7a6 100644 --- a/src/escape.c +++ b/src/escape.c @@ -834,7 +834,9 @@ static inline TERM_ALWAYS_INLINE void dispatch_csi(struct term *t, const struct case 'q': { if (csi->prefix == '>' && !csi->intermediate && (!csi->nparams || !csi->params[0])) { - send_response(t, "\033P>|wsap_term\033\\"); + char resp[96]; + snprintf(resp, sizeof(resp), "\033P>|%s\033\\", t->name); + send_response(t, resp); } else if (!csi->prefix && csi->intermediate == ' ') { term_sc_set_cursor_style(t, csi->nparams > 0 ? csi->params[0] : 0); } diff --git a/src/internal.h b/src/internal.h index 8c02c91..5da114d 100644 --- a/src/internal.h +++ b/src/internal.h @@ -208,6 +208,8 @@ struct term { bool custom_tabs; char osc_buf[256]; + /* The host's name. A DCS sequence can change tcap_name, not this. */ + char name[64]; char tcap_name[64]; char *osc_heap; diff --git a/src/osc/misc.c b/src/osc/misc.c index 5d6b506..74b35cf 100644 --- a/src/osc/misc.c +++ b/src/osc/misc.c @@ -53,7 +53,7 @@ void term_osc_misc_dispatch(struct term *t, long code, const char *params) break; case 9: if (t->cb.notify && strncmp(params, "4;", 2)) - t->cb.notify(t->cb.ud, "wsap_term", params); + t->cb.notify(t->cb.ud, t->name, params); break; case 22: free(t->pointer_shape); diff --git a/src/screen.c b/src/screen.c index ab912a1..0857f17 100644 --- a/src/screen.c +++ b/src/screen.c @@ -840,6 +840,17 @@ bool term_tables_ready(void) return atomic_load_explicit(&tables_state, memory_order_relaxed) == 2; } +/* A name is 1 to 63 printable ASCII bytes. DCS replies echo it. */ +static bool name_valid(const char *s) +{ + for (size_t i = 0; s[i]; i++) { + unsigned char c = (unsigned char)s[i]; + if (i >= 63 || c < 0x20 || c > 0x7e) + return false; + } + return true; +} + struct term *term_new(const struct term_options *opt) { tables_init(); @@ -853,6 +864,9 @@ struct term *term_new(const struct term_options *opt) return NULL; if (opt->scrollback < 0 || (unsigned)opt->cursor_style > TERM_CURSOR_BAR) return NULL; + const char *name = opt->name && *opt->name ? opt->name : "term"; + if (!name_valid(name)) + return NULL; int cols = opt->size.x == 0 ? 80 : opt->size.x; int rows = opt->size.y == 0 ? 24 : opt->size.y; @@ -860,6 +874,7 @@ struct term *term_new(const struct term_options *opt) struct term *t = calloc(1, sizeof(*t)); if (!t) return NULL; + memcpy(t->name, name, strlen(name) + 1); atomic_init(&t->seq, 0); atomic_init(&t->writer_tid, 0); t->write_depth = 0; @@ -906,7 +921,7 @@ struct term *term_new(const struct term_options *opt) t->autowrap = true; t->charsets[0] = t->charsets[1] = 'B'; t->saved = term_saved_cursor_initial(); - memcpy(t->tcap_name, "wsap_term", sizeof("wsap_term")); + memcpy(t->tcap_name, t->name, sizeof(t->name)); term_tabs_default(t); t->full_gen = 1; memset(t->text_width.lo, 0xFF, sizeof(t->text_width.lo)); diff --git a/tests/dcs_test.c b/tests/dcs_test.c index 0e67f78..d9455be 100644 --- a/tests/dcs_test.c +++ b/tests/dcs_test.c @@ -176,8 +176,8 @@ static void test_xtgettcap(void) { struct term *t = new_term(80, 24); assert_tcap(t, "kind", "\033[1;2B"); - assert_tcap(t, "TN", "wsap_term"); - assert_tcap(t, "name", "wsap_term"); + assert_tcap(t, "TN", "term"); + assert_tcap(t, "name", "term"); assert_tcap(t, "Co", "16777216"); assert_tcap(t, "colors", "16777216"); assert_tcap(t, "RGB", "8/8/8"); @@ -230,7 +230,7 @@ static void test_xtgettcap_multi(void) snprintf(req, sizeof(req), "\033P+q%s;%s\033\\", hk, htn); snprintf(want, sizeof(want), "\033P1+r%s=%s;%s=%s\033\\", - hk, hv, htn, "777361705f7465726d"); + hk, hv, htn, "7465726d"); response_len = 0; write_seq(t, req); expect_response(want); @@ -383,7 +383,51 @@ static void test_xtversion(void) struct term *t = new_term(80, 24); response_len = 0; write_seq(t, "\033[>0q"); - expect_response("\033P>|wsap_term\033\\"); + expect_response("\033P>|term\033\\"); + term_free(t); +} + +/* term_options.name feeds XTVERSION and the TN and name capabilities. */ +static void test_host_name(void) +{ + struct term_options opt = { + .size = { .x = 80, .y = 24 }, + .scrollback = TEST_SCROLLBACK, + .name = "myterm", + .callbacks = { .reply = respond }, + }; + struct term *t = term_new(&opt); + assert(t); + response_len = 0; + write_seq(t, "\033[>0q"); + expect_response("\033P>|myterm\033\\"); + assert_tcap(t, "TN", "myterm"); + assert_tcap(t, "name", "myterm"); + + /* A DCS +p sets the capability value. The XTVERSION name does not change. */ + response_len = 0; + write_seq(t, "\033P+p6f74686572\033\\"); + expect_no_response(); + assert_tcap(t, "TN", "other"); + response_len = 0; + write_seq(t, "\033[>0q"); + expect_response("\033P>|myterm\033\\"); + term_free(t); + + /* NULL and "" both select the default. */ + opt.name = ""; + t = term_new(&opt); + assert(t); + response_len = 0; + write_seq(t, "\033[>0q"); + expect_response("\033P>|term\033\\"); + term_free(t); + opt.name = NULL; + t = term_new(&opt); + assert(t); + response_len = 0; + write_seq(t, "\033[>0q"); + expect_response("\033P>|term\033\\"); term_free(t); } @@ -467,6 +511,7 @@ int main(void) test_synchronized_output(); test_ignored_dcs(); test_xtversion(); + test_host_name(); test_bel_terminator(); test_dcs_long_query(); test_dcs_abort_c0(); diff --git a/tests/model.c b/tests/model.c index 6e368ba..d212acb 100644 --- a/tests/model.c +++ b/tests/model.c @@ -72,6 +72,7 @@ enum { GROUND, ESC, CSI, STR, STR_ESC, CHARSET, HASH, DOCS }; enum { OSC, DCS, IGNORE }; struct model { + char name[64]; /* XTVERSION reply */ int cols, rows, limit; struct row **scr; struct row **hist; @@ -1625,7 +1626,7 @@ static void csi_dispatch(struct model *m, unsigned char final) break; case 'q': if (m->prefix == '>' && !m->inter && (!m->nparam || !m->param[0])) { - reply(m, "\033P>|wsap_term\033\\"); + reply(m, "\033P>|%s\033\\", m->name); } else if (!m->prefix && m->inter == ' ') { int ps = m->nparam ? m->param[0] : 0; if (ps == 0) { @@ -1971,10 +1972,11 @@ static void feed_byte(struct model *m, unsigned char c) /* ---- public ---- */ -struct model *model_new(struct term_vector2i size, int history) +struct model *model_new(struct term_vector2i size, int history, const char *name) { struct model *m = calloc(1, sizeof(*m)); if (!m) abort(); + snprintf(m->name, sizeof(m->name), "%s", name && *name ? name : "term"); m->cols = size.x < 1 ? 80 : size.x; m->rows = size.y < 1 ? 24 : size.y; m->limit = history_limit(history); diff --git a/tests/model.h b/tests/model.h index 9a4316f..1707ef7 100644 --- a/tests/model.h +++ b/tests/model.h @@ -28,8 +28,9 @@ struct model_cell { struct model; -/* history is the history limit, as in term_options.scrollback. */ -struct model *model_new(struct term_vector2i size, int history); +/* history is the history limit, as in term_options.scrollback. name is + * as in term_options.name. */ +struct model *model_new(struct term_vector2i size, int history, const char *name); void model_free(struct model *m); void model_feed(struct model *m, const char *s, size_t len, unsigned flags); bool model_resize(struct model *m, struct term_vector2i size); diff --git a/tests/model_row_test.c b/tests/model_row_test.c index 7afca05..ad4ac2c 100644 --- a/tests/model_row_test.c +++ b/tests/model_row_test.c @@ -5,7 +5,7 @@ int main(void) { - struct model *m = model_new((struct term_vector2i){ .x = 10, .y = 3 }, 10); + struct model *m = model_new((struct term_vector2i){ .x = 10, .y = 3 }, 10, NULL); model_feed(m, "A\r\nB\r\nC\r\nD", strlen("A\r\nB\r\nC\r\nD"), 0); assert(model_history(m) == 1); assert(model_row(m, -1) == NULL); diff --git a/tests/model_test.c b/tests/model_test.c index 395097e..e94307c 100644 --- a/tests/model_test.c +++ b/tests/model_test.c @@ -256,7 +256,7 @@ static int run(const struct script *s, enum chunking chunks, uint64_t chunk_seed .callbacks = { .ud = &got, .reply = on_reply }, }; struct term *t = term_new(&opt); - struct model *m = model_new(s->size, s->history); + struct model *m = model_new(s->size, s->history, NULL); struct term_snapshot *snap = term_snapshot_new(); if (!t || !snap) abort(); uint64_t saved_rng = rng; diff --git a/tests/osc_test.c b/tests/osc_test.c index 41a9247..9d08157 100644 --- a/tests/osc_test.c +++ b/tests/osc_test.c @@ -104,6 +104,35 @@ static void reset_captures(void) notify_count = 0; } +/* The notification default is term_options.name. A DCS +p does not change it. */ +static void test_host_name_notify(void) +{ + struct term_options opt = { + .size = { .x = 80, .y = 24 }, + .scrollback = TEST_SCROLLBACK, + .name = "myterm", + .callbacks = { .notify = capture_notify }, + }; + struct term *t = term_new(&opt); + assert(t); + reset_captures(); + write_seq(t, "\033]9;hello\007"); + assert(notify_count == 1); + assert(!strcmp(notify_title, "myterm")); + assert(!strcmp(notify_body, "hello")); + + write_seq(t, "\033P+p6f74686572\033\\"); + reset_captures(); + write_seq(t, "\033]9;again\007"); + assert(notify_count == 1); + assert(!strcmp(notify_title, "myterm")); + + write_seq(t, "\033]777;notify;Title;Body\007"); + assert(notify_count == 2); + assert(!strcmp(notify_title, "Title")); + term_free(t); +} + static void test_osc52(void) { struct term *t = osc_test_term(); @@ -289,7 +318,7 @@ static void test_osc_misc(void) write_seq(t, "\033]9;hello there\033\\"); assert(notify_count == 1); - assert(!strcmp(notify_title, "wsap_term")); + assert(!strcmp(notify_title, "term")); assert(!strcmp(notify_body, "hello there")); write_seq(t, "\033]9;4;3;50\007"); assert(notify_count == 1); @@ -450,6 +479,7 @@ int main(void) test_osc8(); test_sgr_reset_keeps_link(); test_osc_misc(); + test_host_name_notify(); test_osc_colors(); test_osc_legacy_titles(); test_palette_and_theme(); diff --git a/tests/term_test.c b/tests/term_test.c index 7b531b1..51544ef 100644 --- a/tests/term_test.c +++ b/tests/term_test.c @@ -1534,6 +1534,32 @@ static void test_string_payloads(void) } } +/* term_new() refuses a name that a DCS reply could echo unsafely. */ +static void test_host_name_rejected(void) +{ + static const char *const bad[] = { + "\033x", "a\tb", "\177", "\300", "a\nb", + }; + char longest[65]; + memset(longest, 'a', 64); + longest[64] = 0; + char fits[64]; + memset(fits, 'b', 63); + fits[63] = 0; + + for (size_t i = 0; i < sizeof(bad) / sizeof(bad[0]); i++) { + struct term_options opt = { .size = { .x = 10, .y = 3 }, .name = bad[i] }; + assert(!term_new(&opt)); + } + struct term_options opt = { .size = { .x = 10, .y = 3 }, .name = longest }; + assert(!term_new(&opt)); + + opt.name = fits; + struct term *t = term_new(&opt); + assert(t); + term_free(t); +} + static void test_newline_policy_survives_reset(void) { static const char *const resets[] = { "\033c", "\033[!p", "\033[20l" }; @@ -1932,6 +1958,7 @@ int main(void) test_snapshot_follows_new_terminal(); test_snapshot_null_and_range(); test_bulk_rows_survive_edits(); + test_host_name_rejected(); assert(!strcmp(UNICODE_DATA_VERSION, "18.0.0")); assert(!strcmp(term_unicode_version(), UNICODE_DATA_VERSION)); |
