diff --git a/CHANGELOG.md b/CHANGELOG.md index 564b62b2..1d0f5454 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -62,6 +62,11 @@ ([#1807][1807]). * `strikeout-thickness` option. * Implemented the new `xdg-toplevel-icon-v1` protocol. +* Implemented `CSI 20 t`: report window icon. +* Implemented `CSI 21 t`: report window title. +* Implemented `CSI 22 ; 1 t`: push window icon. +* Implemented `CSI 23 ; 1 t`: pop window icon. +* Implemented `OSC 1`: set window icon. [1807]: https://codeberg.org/dnkl/foot/issues/1807 diff --git a/csi.c b/csi.c index 7c6ea7ca..39821d8d 100644 --- a/csi.c +++ b/csi.c @@ -1249,8 +1249,6 @@ csi_dispatch(struct terminal *term, uint8_t final) case 8: LOG_WARN("unimplemented: resize window in chars"); break; case 9: LOG_WARN("unimplemented: maximize/unmaximize window"); break; case 10: LOG_WARN("unimplemented: to/from full screen"); break; - case 20: LOG_WARN("unimplemented: report icon label"); break; - case 21: LOG_WARN("unimplemented: report window title"); break; case 24: LOG_WARN("unimplemented: resize window (DECSLPP)"); break; case 11: /* report if window is iconified */ @@ -1354,6 +1352,24 @@ csi_dispatch(struct terminal *term, uint8_t final) break; } + case 20: { + const char *icon = term_icon(term); + + char reply[3 + strlen(icon) + 2 + 1]; + int chars = xsnprintf( + reply, sizeof(reply), "\033]L%s\033\\", icon); + term_to_slave(term, reply, chars); + break; + } + + case 21: { + char reply[3 + strlen(term->window_title) + 2 + 1]; + int chars = xsnprintf( + reply, sizeof(reply), "\033]l%s\033\\", term->window_title); + term_to_slave(term, reply, chars); + break; + } + case 22: { /* push window title */ /* 0 - icon + title, 1 - icon, 2 - title */ unsigned what = vt_param_get(term, 1, 0); @@ -1361,6 +1377,10 @@ csi_dispatch(struct terminal *term, uint8_t final) tll_push_back( term->window_title_stack, xstrdup(term->window_title)); } + if (what == 0 || what == 1) { + tll_push_back( + term->window_icon_stack, xstrdup(term->window_icon)); + } break; } @@ -1374,6 +1394,13 @@ csi_dispatch(struct terminal *term, uint8_t final) free(title); } } + if (what == 0 || what == 1) { + if (tll_length(term->window_icon_stack) > 0) { + char *icon = tll_pop_back(term->window_icon_stack); + term_set_icon(term, icon); + free(icon); + } + } break; } diff --git a/doc/foot-ctlseqs.7.scd b/doc/foot-ctlseqs.7.scd index 998b6843..60f78d83 100644 --- a/doc/foot-ctlseqs.7.scd +++ b/doc/foot-ctlseqs.7.scd @@ -388,15 +388,27 @@ manipulation sequences. The generic format is: | 19 : - : Report screen size, in characters. +| 20 +: - +: Report icon label. +| 21 +: - +: Report window title. | 22 : - -: Push window title+icon. Foot does not support pushing the icon. +: Push window title+icon. +| 22 +: 1 +: Push window icon. | 22 : 2 : Push window title. | 23 : - -: Pop window title+icon. Foot does not support popping the icon. +: Pop window title+icon. +| 23 +: 1 +: Pop window icon. | 23 : 2 : Pop window title. @@ -659,8 +671,10 @@ All _OSC_ sequences begin with *\\E]*, sometimes abbreviated _OSC_. :< *Description* | \\E] 0 ; _Pt_ \\E\\ : xterm -: Set window icon and title to _Pt_ (foot does not support setting the - icon) +: Set window icon and title to _Pt_. +| \\E] 1 ; _Pt_ \\E\\ +: xterm +: Set window icon to _Pt_. | \\E] 2 ; _Pt_ \\E\\ : xterm : Set window title to _Pt_ diff --git a/misc.c b/misc.c index a81aa9e4..1e5b9328 100644 --- a/misc.c +++ b/misc.c @@ -42,3 +42,10 @@ timespec_sub(const struct timespec *a, const struct timespec *b, res->tv_nsec += one_sec_in_ns; } } + +bool +is_valid_utf8(const char *value) +{ + return value != NULL && + mbsntoc32(NULL, value, strlen(value), 0) != (size_t)-1; +} diff --git a/misc.h b/misc.h index 648bb65f..cce8d2c1 100644 --- a/misc.h +++ b/misc.h @@ -8,3 +8,5 @@ bool isword(char32_t wc, bool spaces_only, const char32_t *delimiters); void timespec_add(const struct timespec *a, const struct timespec *b, struct timespec *res); void timespec_sub(const struct timespec *a, const struct timespec *b, struct timespec *res); + +bool is_valid_utf8(const char *value); diff --git a/osc.c b/osc.c index 0db11811..541a13f4 100644 --- a/osc.c +++ b/osc.c @@ -1145,9 +1145,18 @@ osc_dispatch(struct terminal *term) char *string = (char *)&term->vt.osc.data[data_ofs]; switch (param) { - case 0: term_set_window_title(term, string); break; /* icon + title */ - case 1: break; /* icon */ - case 2: term_set_window_title(term, string); break; /* title */ + case 0: /* icon + title */ + term_set_window_title(term, string); + term_set_icon(term, string); + break; + + case 1: /* icon */ + term_set_icon(term, string); + break; + + case 2: /* title */ + term_set_window_title(term, string); + break; case 4: { /* Set color */ diff --git a/render.c b/render.c index 93bad7fe..355aa40e 100644 --- a/render.c +++ b/render.c @@ -12,15 +12,19 @@ #include "macros.h" #if HAS_INCLUDE() -#include -#define pthread_setname_np(thread, name) (pthread_set_name_np(thread, name), 0) + #include + #define pthread_setname_np(thread, name) (pthread_set_name_np(thread, name), 0) #elif defined(__NetBSD__) -#define pthread_setname_np(thread, name) pthread_setname_np(thread, "%s", (void *)name) + #define pthread_setname_np(thread, name) pthread_setname_np(thread, "%s", (void *)name) #endif +#include #include #include -#include + +#if defined(HAVE_XDG_TOPLEVEL_ICON) +#include +#endif #include @@ -4951,26 +4955,49 @@ render_refresh_app_id(struct terminal *term) term->app_id != NULL ? term->app_id : term->conf->app_id; xdg_toplevel_set_app_id(term->window->xdg_toplevel, app_id); - -#if defined(HAVE_XDG_TOPLEVEL_ICON) - if (term->wl->toplevel_icon_manager != NULL) { - struct xdg_toplevel_icon_v1 *icon = - xdg_toplevel_icon_manager_v1_create_icon( - term->wl->toplevel_icon_manager); - - xdg_toplevel_icon_v1_set_name( - icon, streq(app_id, "footclient") ? "foot" : app_id); - - xdg_toplevel_icon_manager_v1_set_icon( - term->wl->toplevel_icon_manager, term->window->xdg_toplevel, icon); - - xdg_toplevel_icon_v1_destroy(icon); - } -#endif - term->render.app_id.last_update = now; } +void +render_refresh_icon(struct terminal *term) +{ +#if defined(HAVE_XDG_TOPLEVEL_ICON) + if (term->wl->toplevel_icon_manager == NULL) { + LOG_DBG("compositor does not implement xdg-toplevel-icon: " + "ignoring request to refresh window icon"); + return; + } + + struct timespec now; + if (clock_gettime(CLOCK_MONOTONIC, &now) < 0) + return; + + struct timespec diff; + timespec_sub(&now, &term->render.icon.last_update, &diff); + + if (diff.tv_sec == 0 && diff.tv_nsec < 8333 * 1000) { + const struct itimerspec timeout = { + .it_value = {.tv_nsec = 8333 * 1000 - diff.tv_nsec}, + }; + + timerfd_settime(term->render.icon.timer_fd, 0, &timeout, NULL); + return; + } + + const char *icon_name = term_icon(term); + LOG_DBG("setting toplevel icon: %s", icon_name); + + struct xdg_toplevel_icon_v1 *icon = + xdg_toplevel_icon_manager_v1_create_icon(term->wl->toplevel_icon_manager); + xdg_toplevel_icon_v1_set_name(icon, icon_name); + xdg_toplevel_icon_manager_v1_set_icon( + term->wl->toplevel_icon_manager, term->window->xdg_toplevel, icon); + xdg_toplevel_icon_v1_destroy(icon); + + term->render.icon.last_update = now; +#endif +} + void render_refresh(struct terminal *term) { diff --git a/render.h b/render.h index cfedf311..1898351c 100644 --- a/render.h +++ b/render.h @@ -22,6 +22,7 @@ bool render_resize( void render_refresh(struct terminal *term); void render_refresh_app_id(struct terminal *term); +void render_refresh_icon(struct terminal *term); void render_refresh_csd(struct terminal *term); void render_refresh_search(struct terminal *term); void render_refresh_title(struct terminal *term); diff --git a/terminal.c b/terminal.c index 453798e8..5e9a60a5 100644 --- a/terminal.c +++ b/terminal.c @@ -639,6 +639,30 @@ fdm_title_update_timeout(struct fdm *fdm, int fd, int events, void *data) return true; } +static bool +fdm_icon_update_timeout(struct fdm *fdm, int fd, int events, void *data) +{ + if (events & EPOLLHUP) + return false; + + struct terminal *term = data; + uint64_t unused; + ssize_t ret = read(term->render.icon.timer_fd, &unused, sizeof(unused)); + + if (ret < 0) { + if (errno == EAGAIN) + return true; + LOG_ERRNO("failed to read icon update throttle timer"); + return false; + } + + struct itimerspec reset = {{0}}; + timerfd_settime(term->render.icon.timer_fd, 0, &reset, NULL); + + render_refresh_icon(term); + return true; +} + static bool fdm_app_id_update_timeout(struct fdm *fdm, int fd, int events, void *data) { @@ -1114,6 +1138,7 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, int delay_upper_fd = -1; int app_sync_updates_fd = -1; int title_update_fd = -1; + int icon_update_fd = -1; int app_id_update_fd = -1; struct terminal *term = malloc(sizeof(*term)); @@ -1150,6 +1175,12 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, goto close_fds; } + if ((icon_update_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0) + { + LOG_ERRNO("failed to create icon update throttle timer FD"); + goto close_fds; + } + if ((app_id_update_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0) { LOG_ERRNO("failed to create app ID update throttle timer FD"); @@ -1187,6 +1218,7 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, !fdm_add(fdm, delay_upper_fd, EPOLLIN, &fdm_delayed_render, term) || !fdm_add(fdm, app_sync_updates_fd, EPOLLIN, &fdm_app_sync_updates_timeout, term) || !fdm_add(fdm, title_update_fd, EPOLLIN, &fdm_title_update_timeout, term) || + !fdm_add(fdm, icon_update_fd, EPOLLIN, &fdm_icon_update_timeout, term) || !fdm_add(fdm, app_id_update_fd, EPOLLIN, &fdm_app_id_update_timeout, term)) { goto err; @@ -1288,6 +1320,9 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, .title = { .timer_fd = title_update_fd, }, + .icon = { + .timer_fd = icon_update_fd, + }, .app_id = { .timer_fd = app_id_update_fd, }, @@ -1406,6 +1441,7 @@ close_fds: fdm_del(fdm, delay_upper_fd); fdm_del(fdm, app_sync_updates_fd); fdm_del(fdm, title_update_fd); + fdm_del(fdm, icon_update_fd); fdm_del(fdm, app_id_update_fd); free(term); @@ -1626,6 +1662,7 @@ term_shutdown(struct terminal *term) fdm_del(term->fdm, term->selection.auto_scroll.fd); fdm_del(term->fdm, term->render.app_sync_updates.timer_fd); fdm_del(term->fdm, term->render.app_id.timer_fd); + fdm_del(term->fdm, term->render.icon.timer_fd); fdm_del(term->fdm, term->render.title.timer_fd); fdm_del(term->fdm, term->delayed_render_timer.lower_fd); fdm_del(term->fdm, term->delayed_render_timer.upper_fd); @@ -1677,6 +1714,7 @@ term_shutdown(struct terminal *term) term->selection.auto_scroll.fd = -1; term->render.app_sync_updates.timer_fd = -1; term->render.app_id.timer_fd = -1; + term->render.icon.timer_fd = -1; term->render.title.timer_fd = -1; term->delayed_render_timer.lower_fd = -1; term->delayed_render_timer.upper_fd = -1; @@ -1731,6 +1769,7 @@ term_destroy(struct terminal *term) fdm_del(term->fdm, term->selection.auto_scroll.fd); fdm_del(term->fdm, term->render.app_sync_updates.timer_fd); fdm_del(term->fdm, term->render.app_id.timer_fd); + fdm_del(term->fdm, term->render.icon.timer_fd); fdm_del(term->fdm, term->render.title.timer_fd); fdm_del(term->fdm, term->delayed_render_timer.lower_fd); fdm_del(term->fdm, term->delayed_render_timer.upper_fd); @@ -1775,7 +1814,9 @@ term_destroy(struct terminal *term) composed_free(term->composed); free(term->app_id); + free(term->window_icon); free(term->window_title); + tll_free_and_free(term->window_icon_stack, free); tll_free_and_free(term->window_title_stack, free); for (size_t i = 0; i < sizeof(term->fonts) / sizeof(term->fonts[0]); i++) @@ -2007,6 +2048,9 @@ term_reset(struct terminal *term, bool hard) term->saved_charsets = term->charsets; tll_free_and_free(term->window_title_stack, free); term_set_window_title(term, term->conf->title); + tll_free_and_free(term->window_icon_stack, free); + term_set_app_id(term, NULL); + term_set_icon(term, NULL); term_set_user_mouse_cursor(term, NULL); @@ -3528,7 +3572,7 @@ term_set_window_title(struct terminal *term, const char *title) if (term->window_title != NULL && streq(term->window_title, title)) return; - if (mbsntoc32(NULL, title, strlen(title), 0) == (size_t)-1) { + if (!is_valid_utf8(title)) { /* It's an xdg_toplevel::set_title() protocol violation to set a title with an invalid UTF-8 sequence */ LOG_WARN("%s: title is not valid UTF-8, ignoring", title); @@ -3548,9 +3592,14 @@ term_set_app_id(struct terminal *term, const char *app_id) app_id = NULL; if (term->app_id == NULL && app_id == NULL) return; - if (term->app_id != NULL && app_id != NULL && strcmp(term->app_id, app_id) == 0) + if (term->app_id != NULL && app_id != NULL && streq(term->app_id, app_id)) return; + if (app_id != NULL && !is_valid_utf8(app_id)) { + LOG_WARN("%s: app-id is not valid UTF-8, ignoring", app_id); + return; + } + free(term->app_id); if (app_id != NULL) { term->app_id = xstrdup(app_id); @@ -3558,6 +3607,44 @@ term_set_app_id(struct terminal *term, const char *app_id) term->app_id = NULL; } render_refresh_app_id(term); + render_refresh_icon(term); +} + +void +term_set_icon(struct terminal *term, const char *icon) +{ + if (icon != NULL && *icon == '\0') + icon = NULL; + if (term->window_icon == NULL && icon == NULL) + return; + if (term->window_icon != NULL && icon != NULL && streq(term->window_icon, icon)) + return; + + if (icon != NULL && !is_valid_utf8(icon)) { + LOG_WARN("%s: icon label is not valid UTF-8, ignoring", icon); + return; + } + + free(term->window_icon); + if (icon != NULL) { + term->window_icon = xstrdup(icon); + } else { + term->window_icon = NULL; + } + render_refresh_icon(term); +} + +const char * +term_icon(const struct terminal *term) +{ + const char *app_id = + term->app_id != NULL ? term->app_id : term->conf->app_id; + + return term->window_icon != NULL + ? term->window_icon + : streq(app_id, "footclient") + ? "foot" + : app_id; } void diff --git a/terminal.h b/terminal.h index a87a125b..28576f23 100644 --- a/terminal.h +++ b/terminal.h @@ -552,6 +552,8 @@ struct terminal { bool window_title_has_been_set; char *window_title; tll(char *) window_title_stack; + char *window_icon; + tll(char *)window_icon_stack; char *app_id; struct { @@ -670,6 +672,11 @@ struct terminal { int timer_fd; } title; + struct { + struct timespec last_update; + int timer_fd; + } icon; + struct { struct timespec last_update; int timer_fd; @@ -925,6 +932,8 @@ void term_set_user_mouse_cursor(struct terminal *term, const char *cursor); void term_set_window_title(struct terminal *term, const char *title); void term_set_app_id(struct terminal *term, const char *app_id); +void term_set_icon(struct terminal *term, const char *icon); +const char *term_icon(const struct terminal *term); void term_flash(struct terminal *term, unsigned duration_ms); void term_bell(struct terminal *term); bool term_spawn_new(const struct terminal *term);