diff --git a/CHANGELOG.md b/CHANGELOG.md index d8cadf79..622617b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,6 +61,8 @@ (and the grid reflowed) or not when e.g. zooming in/out ([#1807][1807]). * `strikeout-thickness` option. +* Implemented the new `xdg-toplevel-icon-v1` protocol. +* Implemented `CSI 21 t`: report window title. [1807]: https://codeberg.org/dnkl/foot/issues/1807 diff --git a/csi.c b/csi.c index 7c6ea7ca..35a39f82 100644 --- a/csi.c +++ b/csi.c @@ -1250,7 +1250,6 @@ csi_dispatch(struct terminal *term, uint8_t final) 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 +1353,14 @@ csi_dispatch(struct terminal *term, uint8_t final) 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); 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/foot-features.h b/foot-features.h index ad447767..674c1056 100644 --- a/foot-features.h +++ b/foot-features.h @@ -37,3 +37,12 @@ static inline bool feature_graphemes(void) return false; #endif } + +static inline bool feature_xdg_toplevel_icon(void) +{ +#if defined(HAVE_XDG_TOPLEVEL_ICON) + return true; +#else + return false; +#endif +} diff --git a/main.c b/main.c index 15741012..973cbae4 100644 --- a/main.c +++ b/main.c @@ -51,11 +51,12 @@ version_and_features(void) { static char buf[256]; snprintf(buf, sizeof(buf), - "version: %s %cpgo %cime %cgraphemes %cassertions", + "version: %s %cpgo %cime %cgraphemes %ctoplevel-icon %cassertions", FOOT_VERSION, feature_pgo() ? '+' : '-', feature_ime() ? '+' : '-', feature_graphemes() ? '+' : '-', + feature_xdg_toplevel_icon() ? '+' : '-', feature_assertions() ? '+' : '-'); return buf; } diff --git a/meson.build b/meson.build index 1b3b01aa..91e745bf 100644 --- a/meson.build +++ b/meson.build @@ -171,6 +171,14 @@ wl_proto_xml = [ wayland_protocols_datadir / 'staging/single-pixel-buffer/single-pixel-buffer-v1.xml', ] +if wayland_protocols.version().version_compare('>=1.37') + add_project_arguments('-DHAVE_XDG_TOPLEVEL_ICON', language: 'c') + wl_proto_xml += [wayland_protocols_datadir / 'staging/xdg-toplevel-icon/xdg-toplevel-icon-v1.xml'] + xdg_toplevel_icon = true +else + xdg_toplevel_icon = false +endif + foreach prot : wl_proto_xml wl_proto_headers += custom_target( prot.underscorify() + '-client-header', @@ -401,6 +409,7 @@ summary( 'Themes': get_option('themes'), 'IME': get_option('ime'), 'Grapheme clustering': utf8proc.found(), + 'Wayland: xdg-toplevel-icon-v1': xdg_toplevel_icon, 'utmp backend': utmp_backend, 'utmp helper default path': utmp_default_helper_path, 'Build terminfo': tic.found(), 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..5efc7588 100644 --- a/osc.c +++ b/osc.c @@ -1145,9 +1145,16 @@ 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); + break; + + case 1: /* icon */ + break; + + case 2: /* title */ + term_set_window_title(term, string); + break; case 4: { /* Set color */ diff --git a/pgo/pgo.c b/pgo/pgo.c index f87863c0..aab18847 100644 --- a/pgo/pgo.c +++ b/pgo/pgo.c @@ -70,6 +70,7 @@ void render_refresh(struct terminal *term) {} void render_refresh_csd(struct terminal *term) {} void render_refresh_title(struct terminal *term) {} void render_refresh_app_id(struct terminal *term) {} +void render_refresh_icon(struct terminal *term) {} bool render_xcursor_is_valid(const struct seat *seat, const char *cursor) diff --git a/render.c b/render.c index 00ea3445..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 @@ -4944,10 +4948,54 @@ render_refresh_app_id(struct terminal *term) }; timerfd_settime(term->render.app_id.timer_fd, 0, &timeout, NULL); - } else { - term->render.app_id.last_update = now; - xdg_toplevel_set_app_id(term->window->xdg_toplevel, term->app_id ? term->app_id : term->conf->app_id); + return; } + + const char *app_id = + term->app_id != NULL ? term->app_id : term->conf->app_id; + + xdg_toplevel_set_app_id(term->window->xdg_toplevel, app_id); + 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 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..6d62a3ed 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); @@ -2007,6 +2046,7 @@ 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); + term_set_app_id(term, NULL); term_set_user_mouse_cursor(term, NULL); @@ -3528,7 +3568,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 +3588,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 +3603,24 @@ term_set_app_id(struct terminal *term, const char *app_id) term->app_id = NULL; } render_refresh_app_id(term); + 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 +#if 0 +term->window_icon != NULL + ? term->window_icon + : + #endif + streq(app_id, "footclient") + ? "foot" + : app_id; } void diff --git a/terminal.h b/terminal.h index a87a125b..e87df54c 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; /* No escape sequence available to set the 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,7 @@ 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); +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); diff --git a/wayland.c b/wayland.c index 3f65901b..9c184adc 100644 --- a/wayland.c +++ b/wayland.c @@ -1363,6 +1363,17 @@ handle_global(void *data, struct wl_registry *registry, &wp_single_pixel_buffer_manager_v1_interface, required); } +#if defined(HAVE_XDG_TOPLEVEL_ICON) + else if (streq(interface, xdg_toplevel_icon_v1_interface.name)) { + const uint32_t required = 1; + if (!verify_iface_version(interface, version, required)) + return; + + wayl->toplevel_icon_manager = wl_registry_bind( + wayl->registry, name, &xdg_toplevel_icon_v1_interface, required); + } +#endif + #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED else if (streq(interface, zwp_text_input_manager_v3_interface.name)) { const uint32_t required = 1; @@ -1581,27 +1592,33 @@ wayl_init(struct fdm *fdm, struct key_binding_manager *key_binding_manager, goto out; } + if (presentation_timings && wayl->presentation == NULL) { + LOG_ERR("compositor does not implement the presentation time interface"); + goto out; + } + if (wayl->primary_selection_device_manager == NULL) - LOG_WARN("no primary selection available"); + LOG_WARN("compositor does not implement the primary selection interface"); if (wayl->xdg_activation == NULL) { LOG_WARN( - "no XDG activation support; " + "compositor does not implement XDG activation, " "bell.urgent will fall back to coloring the window margins red"); } if (wayl->fractional_scale_manager == NULL || wayl->viewporter == NULL) - LOG_WARN("fractional scaling not available"); + LOG_WARN("compositor does not implement fractional scaling"); if (wayl->cursor_shape_manager == NULL) { - LOG_WARN("no server-side cursors available, " + LOG_WARN("compositor does not implement server-side cursors, " "falling back to client-side cursors"); } - if (presentation_timings && wayl->presentation == NULL) { - LOG_ERR("presentation time interface not implemented by compositor"); - goto out; +#if defined(HAVE_XDG_TOPLEVEL_ICON) + if (wayl->toplevel_icon_manager == NULL) { + LOG_WARN("compositor does not implement the XDG toplevel icon protocol"); } +#endif #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED if (wayl->text_input_manager == NULL) { @@ -1679,6 +1696,10 @@ wayl_destroy(struct wayland *wayl) zwp_text_input_manager_v3_destroy(wayl->text_input_manager); #endif +#if defined(HAVE_XDG_TOPLEVEL_ICON) + if (wayl->toplevel_icon_manager != NULL) + xdg_toplevel_icon_manager_v1_destroy(wayl->toplevel_icon_manager); +#endif if (wayl->single_pixel_manager != NULL) wp_single_pixel_buffer_manager_v1_destroy(wayl->single_pixel_manager); if (wayl->fractional_scale_manager != NULL) @@ -1796,6 +1817,21 @@ wayl_win_init(struct terminal *term, const char *token) xdg_toplevel_set_app_id(win->xdg_toplevel, conf->app_id); +#if defined(HAVE_XDG_TOPLEVEL_ICON) + if (wayl->toplevel_icon_manager != NULL) { + const char *app_id = + term->app_id != NULL ? term->app_id : term->conf->app_id; + + struct xdg_toplevel_icon_v1 *icon = + xdg_toplevel_icon_manager_v1_create_icon(wayl->toplevel_icon_manager); + xdg_toplevel_icon_v1_set_name(icon, streq( + app_id, "footclient") ? "foot" : app_id); + xdg_toplevel_icon_manager_v1_set_icon( + wayl->toplevel_icon_manager, win->xdg_toplevel, icon); + xdg_toplevel_icon_v1_destroy(icon); + } +#endif + if (conf->csd.preferred == CONF_CSD_PREFER_NONE) { /* User specifically do *not* want decorations */ win->csd_mode = CSD_NO; diff --git a/wayland.h b/wayland.h index ca9c05fa..227e2a68 100644 --- a/wayland.h +++ b/wayland.h @@ -20,6 +20,10 @@ #include #include +#if defined(HAVE_XDG_TOPLEVEL_ICON) + #include +#endif + #include #include @@ -443,6 +447,10 @@ struct wayland { struct wp_single_pixel_buffer_manager_v1 *single_pixel_manager; +#if defined(HAVE_XDG_TOPLEVEL_ICON) + struct xdg_toplevel_icon_manager_v1 *toplevel_icon_manager; +#endif + bool presentation_timings; struct wp_presentation *presentation; uint32_t presentation_clock_id;