config: add toplevel-tag=TAG

Add support for the new xdg-toplevel-tag-v1 Wayland protocol, by
exposing a new config option, `toplevel-tag`, and a corresponding
command option, `--toplevel-tag` (in both `foot` and `footclient`).

This can help the compositor with session management, or custom window
rules.

Closes #2212
This commit is contained in:
Daniel Eklöf 2025-11-12 11:04:25 +01:00
parent c9abab0807
commit fc9625678f
No known key found for this signature in database
GPG key ID: 5BBD4992C116573F
19 changed files with 113 additions and 6 deletions

View file

@ -69,6 +69,15 @@
## Unreleased
### Added
* `toplevel-tag` option (and `--toplevel-tag` command line options to
`foot` and `footclient`), allowing you to set a custom toplevel
tag. The compositor must implement the new `xdg-toplevel-tag-v1`
Wayland protocol ([#2212][2212]).
[2212]: https://codeberg.org/dnkl/foot/issues/2212
### Changed
* When enabling _"focus mode"_ (private mode 1004), foot now sends a

View file

@ -76,6 +76,7 @@ print_usage(const char *prog_name)
" -t,--term=TERM value to set the environment variable TERM to (" FOOT_DEFAULT_TERM ")\n"
" -T,--title=TITLE initial window title (foot)\n"
" -a,--app-id=ID window application ID (foot)\n"
" --toplevel-tag=TAG set a custom toplevel tag\n"
" -w,--window-size-pixels=WIDTHxHEIGHT initial width and height, in pixels\n"
" -W,--window-size-chars=WIDTHxHEIGHT initial width and height, in characters\n"
" -m,--maximized start in maximized mode\n"
@ -137,6 +138,10 @@ send_string_list(int fd, const string_list_t *string_list)
return true;
}
enum {
TOPLEVEL_TAG_OPTION = CHAR_MAX + 1,
};
int
main(int argc, char *const *argv)
{
@ -151,6 +156,7 @@ main(int argc, char *const *argv)
{"term", required_argument, NULL, 't'},
{"title", required_argument, NULL, 'T'},
{"app-id", required_argument, NULL, 'a'},
{"toplevel-tag", required_argument, NULL, TOPLEVEL_TAG_OPTION},
{"window-size-pixels", required_argument, NULL, 'w'},
{"window-size-chars", required_argument, NULL, 'W'},
{"maximized", no_argument, NULL, 'm'},
@ -220,6 +226,12 @@ main(int argc, char *const *argv)
goto err;
break;
case TOPLEVEL_TAG_OPTION:
snprintf(buf, sizeof(buf), "toplevel-tag=%s", optarg);
if (!push_string(&overrides, buf, &total_len))
goto err;
break;
case 'L':
if (!push_string(&overrides, "login-shell=yes", &total_len))
goto err;

View file

@ -6,6 +6,7 @@ _foot()
local cur prev flags word commands match previous_words i offset
flags=(
"--app-id"
"--toplevel-tag"
"--check-config"
"--config"
"--font"
@ -40,7 +41,7 @@ _foot()
for word in "${previous_words[@]}" ; do
match=$(printf "$commands" | grep -Fx "$word" 2>/dev/null)
if [[ ! -z "$match" ]] ; then
if [[ ${COMP_WORDS[i-1]} =~ ^(--app-id|--config|--font|--log-level|--pty|--term|--title|--window-size-pixels|--window-size-chars|--working-directory)$ ]] ; then
if [[ ${COMP_WORDS[i-1]} =~ ^(--app-id|--toplevel-tag|--config|--font|--log-level|--pty|--term|--title|--window-size-pixels|--window-size-chars|--working-directory)$ ]] ; then
(( i++ ))
continue
fi
@ -75,7 +76,7 @@ _foot()
COMPREPLY=( $(compgen -W "none error warning info" -- ${cur}) ) ;;
--log-colorize|-l)
COMPREPLY=( $(compgen -W "never always auto" -- ${cur}) ) ;;
--app-id|--help|--override|--pty|--title|--version|--window-size-chars|--window-size-pixels|--check-config|-[ahoTvWwC])
--app-id|--toplevel-tag|--help|--override|--pty|--title|--version|--window-size-chars|--window-size-pixels|--check-config|-[ahoTvWwC])
# Don't autocomplete for these flags
: ;;
*)

View file

@ -6,6 +6,7 @@ _footclient()
local cur prev flags word commands match previous_words i offset
flags=(
"--app-id"
"--toplevel-tag"
"--fullscreen"
"--help"
"--hold"
@ -35,7 +36,7 @@ _footclient()
for word in "${previous_words[@]}" ; do
match=$(printf "$commands" | grep -Fx "$word" 2>/dev/null)
if [[ ! -z "$match" ]] ; then
if [[ ${COMP_WORDS[i-1]} =~ ^(--app-id|--log-level|--server-socket|--term|--title|--window-size-pixels|--window-size-chars|--working-directory)$ ]] ; then
if [[ ${COMP_WORDS[i-1]} =~ ^(--app-id|--toplevel-tag|--log-level|--server-socket|--term|--title|--window-size-pixels|--window-size-chars|--working-directory)$ ]] ; then
(( i++ ))
continue
fi
@ -67,7 +68,7 @@ _footclient()
COMPREPLY=( $(compgen -W "none error warning info" -- ${cur}) ) ;;
--log-colorize|-l)
COMPREPLY=( $(compgen -W "never always auto" -- ${cur}) ) ;;
--app-id|--help|--override|--title|--version|--window-size-chars|--window-size-pixels|-[ahoTvWw])
--app-id|--toplevel-tag|--help|--override|--title|--version|--window-size-chars|--window-size-pixels|-[ahoTvWw])
# Don't autocomplete for these flags
: ;;
*)

View file

@ -6,6 +6,7 @@ complete -c foot -x -s f -l font -a "(fc-list : family | sed 's/,/
complete -c foot -x -s t -l term -a '(find /usr/share/terminfo -type f -printf "%f\n")' -d "value to set the environment variable TERM to (foot)"
complete -c foot -x -s T -l title -d "initial window title"
complete -c foot -x -s a -l app-id -d "value to set the app-id property on the Wayland window to (foot)"
complete -c foot -x -l toplevel-tag -d "value to set the toplevel-tag property on the Wayland window to"
complete -c foot -s m -l maximized -d "start in maximized mode"
complete -c foot -s F -l fullscreen -d "start in fullscreen mode"
complete -c foot -s L -l login-shell -d "start shell as a login shell"

View file

@ -2,6 +2,7 @@ complete -c footclient -x -a "(__fish_complete_subcom
complete -c footclient -x -s t -l term -a '(find /usr/share/terminfo -type f -printf "%f\n")' -d "value to set the environment variable TERM to (foot)"
complete -c footclient -x -s T -l title -d "initial window title"
complete -c footclient -x -s a -l app-id -d "value to set the app-id property on the Wayland window to (foot)"
complete -c footclient -x -l toplevel-tag -d "value to set the toplevel-tag property on the Wayland window to"
complete -c footclient -s m -l maximized -d "start in maximized mode"
complete -c footclient -s F -l fullscreen -d "start in fullscreen mode"
complete -c footclient -s L -l login-shell -d "start shell as a login shell"

View file

@ -9,6 +9,7 @@ _arguments \
'(-t --term)'{-t,--term}'[value to set the environment variable TERM to (foot)]:term:->terms' \
'(-T --title)'{-T,--title}'[initial window title]:()' \
'(-a --app-id)'{-a,--app-id}'[value to set the app-id property on the Wayland window to (foot)]:()' \
'--toplevel-tag=[value to set the toplevel-tag property on the Wayland window to]:()' \
'(-m --maximized)'{-m,--maximized}'[start in maximized mode]' \
'(-F --fullscreen)'{-F,--fullscreen}'[start in fullscreen mode]' \
'(-L --login-shell)'{-L,--login-shell}'[start shell as a login shell]' \

View file

@ -5,6 +5,7 @@ _arguments \
'(-t --term)'{-t,--term}'[value to set the environment variable TERM to (foot)]:term:->terms' \
'(-T --title)'{-T,--title}'[initial window title]:()' \
'(-a --app-id)'{-a,--app-id}'[value to set the app-id property on the Wayland window to (foot)]:()' \
'--toplevel-tag=[value to set the toplevel-tag property on the Wayland window to]:()' \
'(-m --maximized)'{-m,--maximized}'[start in maximized mode]' \
'(-F --fullscreen)'{-F,--fullscreen}'[start in fullscreen mode]' \
'(-L --login-shell)'{-L,--login-shell}'[start shell as a login shell]' \

View file

@ -923,6 +923,9 @@ parse_section_main(struct context *ctx)
else if (streq(key, "app-id"))
return value_to_str(ctx, &conf->app_id);
else if (streq(key, "toplevel-tag"))
return value_to_str(ctx, &conf->toplevel_tag);
else if (streq(key, "initial-window-size-pixels")) {
if (!value_to_dimensions(ctx, &conf->size.width, &conf->size.height))
return false;
@ -3371,6 +3374,7 @@ config_load(struct config *conf, const char *conf_path,
.shell = get_shell(),
.title = xstrdup("foot"),
.app_id = (as_server ? xstrdup("footclient") : xstrdup("foot")),
.toplevel_tag = xstrdup(""),
.word_delimiters = xc32dup(U",│`|:\"'()[]{}<>"),
.size = {
.type = CONF_SIZE_PX,
@ -3823,6 +3827,7 @@ config_clone(const struct config *old)
conf->shell = xstrdup(old->shell);
conf->title = xstrdup(old->title);
conf->app_id = xstrdup(old->app_id);
conf->toplevel_tag = xstrdup(old->toplevel_tag);
conf->word_delimiters = xc32dup(old->word_delimiters);
conf->scrollback.indicator.text = xc32dup(old->scrollback.indicator.text);
conf->server_socket_path = xstrdup(old->server_socket_path);
@ -3922,6 +3927,7 @@ config_free(struct config *conf)
free(conf->shell);
free(conf->title);
free(conf->app_id);
free(conf->toplevel_tag);
free(conf->word_delimiters);
spawn_template_free(&conf->bell.command);
free(conf->scrollback.indicator.text);

View file

@ -219,6 +219,7 @@ struct config {
char *shell;
char *title;
char *app_id;
char *toplevel_tag;
char32_t *word_delimiters;
bool login_shell;
bool locked_title;

View file

@ -67,6 +67,11 @@ the foot command line
Value to set the *app-id* property on the Wayland window
to. Default: _foot_ (normal mode), or _footclient_ (server mode).
*toplevel-tag*=_TAG_
Value to set the *toplevel-tag* property on the Wayland window
to. The compositor can use this value for session management,
window rules etc. Default: _not set_
*-m*,*--maximized*
Start in maximized mode. If both *--maximized* and *--fullscreen*
are specified, the _last_ one takes precedence.

View file

@ -429,6 +429,11 @@ empty string to be set, but it must be quoted: *KEY=""*)
apply window management rules. Default: _foot_ (normal mode), or
_footclient_ (server mode).
*toplevel-tag*
Value to set the *toplevel-tag* property on the Wayland window
to. The compositor can use this value for session management,
window rules etc. Default: _not set_
*bold-text-in-bright*
Semi-boolean. When enabled, bold text is rendered in a brighter
color (in addition to using a bold font). The color is brightened

View file

@ -33,6 +33,11 @@ terminal has terminated.
Value to set the *app-id* property on the Wayland window
to. Default: _foot_ (normal mode), or _footclient_ (server mode).
*toplevel-tag*=_TAG_
Value to set the *toplevel-tag* property on the Wayland window
to. The compositor can use this value for session management,
window rules etc. Default: _not set_
*-w*,*--window-size-pixels*=_WIDTHxHEIGHT_
Set initial window width and height, in pixels. Default: _700x500_.

View file

@ -22,6 +22,12 @@ const char version_and_features[] =
" -graphemes"
#endif
#if defined(HAVE_XDG_TOPLEVEL_TAG)
" +toplevel-tag"
#else
" -toplevel-tag"
#endif
#if !defined(NDEBUG)
" +assertions"
#else

7
main.c
View file

@ -84,6 +84,7 @@ print_usage(const char *prog_name)
" -t,--term=TERM value to set the environment variable TERM to (" FOOT_DEFAULT_TERM ")\n"
" -T,--title=TITLE initial window title (foot)\n"
" -a,--app-id=ID window application ID (foot)\n"
" --toplevel-tag=TAG set a custom toplevel tag\n"
" -m,--maximized start in maximized mode\n"
" -F,--fullscreen start in fullscreen mode\n"
" -L,--login-shell start shell as a login shell\n"
@ -185,6 +186,7 @@ sanitize_signals(void)
enum {
PTY_OPTION = CHAR_MAX + 1,
TOPLEVEL_TAG_OPTION = CHAR_MAX + 2,
};
int
@ -214,6 +216,7 @@ main(int argc, char *const *argv)
{"term", required_argument, NULL, 't'},
{"title", required_argument, NULL, 'T'},
{"app-id", required_argument, NULL, 'a'},
{"toplevel-tag", required_argument, NULL, TOPLEVEL_TAG_OPTION},
{"login-shell", no_argument, NULL, 'L'},
{"working-directory", required_argument, NULL, 'D'},
{"font", required_argument, NULL, 'f'},
@ -285,6 +288,10 @@ main(int argc, char *const *argv)
tll_push_back(overrides, xstrjoin("app-id=", optarg));
break;
case TOPLEVEL_TAG_OPTION:
tll_push_back(overrides, xstrjoin("toplevel-tag=", optarg));
break;
case 'D': {
struct stat st;
if (stat(optarg, &st) < 0 || !(st.st_mode & S_IFDIR)) {

View file

@ -182,7 +182,12 @@ wl_proto_xml = [
wayland_protocols_datadir / 'staging/xdg-toplevel-icon/xdg-toplevel-icon-v1.xml',
wayland_protocols_datadir / 'staging/xdg-system-bell/xdg-system-bell-v1.xml',
wayland_protocols_datadir / 'staging/color-management/color-management-v1.xml',
]
]
if (wayland_protocols.version().version_compare('>=1.43'))
wl_proto_xml += [wayland_protocols_datadir / 'staging/xdg-toplevel-tag/xdg-toplevel-tag-v1.xml']
add_project_arguments('-DHAVE_XDG_TOPLEVEL_TAG=1', language: 'c')
endif
foreach prot : wl_proto_xml
wl_proto_headers += custom_target(

View file

@ -482,6 +482,7 @@ test_section_main(void)
test_string(&ctx, &parse_section_main, "shell", &conf.shell);
test_string(&ctx, &parse_section_main, "term", &conf.term);
test_string(&ctx, &parse_section_main, "app-id", &conf.app_id);
test_string(&ctx, &parse_section_main, "toplevel-tag", &conf.toplevel_tag);
test_string(&ctx, &parse_section_main, "utmp-helper", &conf.utmp_helper_path);
test_c32string(&ctx, &parse_section_main, "word-delimiters", &conf.word_delimiters);

View file

@ -1548,6 +1548,17 @@ handle_global(void *data, struct wl_registry *registry,
wayl->color_management.manager, &color_manager_listener, wayl);
}
#if defined(HAVE_XDG_TOPLEVEL_TAG)
else if (streq(interface, xdg_toplevel_tag_manager_v1_interface.name)) {
const uint32_t required = 1;
if (!verify_iface_version(interface, version, required))
return;
wayl->toplevel_tag_manager = wl_registry_bind(
wayl->registry, name, &xdg_toplevel_tag_manager_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;
@ -1791,7 +1802,7 @@ wayl_init(struct fdm *fdm, struct key_binding_manager *key_binding_manager,
}
if (wayl->toplevel_icon_manager == NULL) {
LOG_WARN("compositor does not implement the XDG toplevel icon protocol");
LOG_WARN("compositor does not implement the xdg-toplevel-icon protocol");
}
#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED
@ -1870,6 +1881,11 @@ wayl_destroy(struct wayland *wayl)
zwp_text_input_manager_v3_destroy(wayl->text_input_manager);
#endif
#if defined(HAVE_XDG_TOPLEVEL_TAG)
if (wayl->toplevel_tag_manager != NULL)
xdg_toplevel_tag_manager_v1_destroy(wayl->toplevel_tag_manager);
#endif
if (wayl->color_management.img_description != NULL)
wp_image_description_v1_destroy(wayl->color_management.img_description);
if (wayl->color_management.manager != NULL)
@ -1995,6 +2011,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_TAG)
if (conf->toplevel_tag != NULL && conf->toplevel_tag[0] != '\0') {
if (wayl->toplevel_tag_manager != NULL) {
xdg_toplevel_tag_manager_v1_set_toplevel_tag(
wayl->toplevel_tag_manager, win->xdg_toplevel, conf->toplevel_tag);
/* TODO: the description is recommended to be the tag, but translated */
xdg_toplevel_tag_manager_v1_set_toplevel_description(
wayl->toplevel_tag_manager, win->xdg_toplevel, conf->toplevel_tag);
} else {
LOG_WARN("compositor does not implement the xdg-toplevel-tag protocol");
}
}
#endif
if (wayl->toplevel_icon_manager != NULL) {
const char *app_id =
term->app_id != NULL ? term->app_id : term->conf->app_id;

View file

@ -23,6 +23,10 @@
#include <xdg-system-bell-v1.h>
#include <xdg-toplevel-icon-v1.h>
#if defined(HAVE_XDG_TOPLEVEL_TAG)
#include <xdg-toplevel-tag-v1.h>
#endif
#include <fcft/fcft.h>
#include <tllist.h>
@ -481,6 +485,10 @@ struct wayland {
struct wp_presentation *presentation;
uint32_t presentation_clock_id;
#if defined(HAVE_XDG_TOPLEVEL_TAG)
struct xdg_toplevel_tag_manager_v1 *toplevel_tag_manager;
#endif
#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED
struct zwp_text_input_manager_v3 *text_input_manager;
#endif