From 5b87139670baf796ddc4e39a8126b034b3c036b9 Mon Sep 17 00:00:00 2001 From: valoq Date: Thu, 28 May 2026 15:22:37 +0200 Subject: [PATCH] add wait-for-mapped option --- config.c | 4 ++ config.h | 1 + doc/foot.1.scd | 4 ++ doc/foot.ini.5.scd | 8 ++++ main.c | 7 ++++ render.c | 3 ++ terminal.c | 92 ++++++++++++++++++++++++++++++++++++++++----- terminal.h | 10 +++++ tests/test-config.c | 1 + 9 files changed, 121 insertions(+), 9 deletions(-) diff --git a/config.c b/config.c index 481d4c4f..90b13a07 100644 --- a/config.c +++ b/config.c @@ -916,6 +916,9 @@ parse_section_main(struct context *ctx) else if (streq(key, "login-shell")) return value_to_bool(ctx, &conf->login_shell); + else if (streq(key, "wait-for-mapped")) + return value_to_bool(ctx, &conf->wait_for_mapped); + else if (streq(key, "title")) return value_to_str(ctx, &conf->title); @@ -3592,6 +3595,7 @@ config_load(struct config *conf, const char *conf_path, .presentation_timings = false, .selection_target = SELECTION_TARGET_PRIMARY, .hold_at_exit = false, + .wait_for_mapped = false, .desktop_notifications = { .command = { .argv = {.args = NULL}, diff --git a/config.h b/config.h index f8e99df3..32bbf6ba 100644 --- a/config.h +++ b/config.h @@ -237,6 +237,7 @@ struct config { char32_t *word_delimiters; bool login_shell; bool locked_title; + bool wait_for_mapped; struct { enum conf_size_type type; diff --git a/doc/foot.1.scd b/doc/foot.1.scd index a190db9b..2b5a4f57 100644 --- a/doc/foot.1.scd +++ b/doc/foot.1.scd @@ -86,6 +86,10 @@ the foot command line *-L*,*--login-shell* Start a login shell, by prepending a '-' to argv[0]. +*--wait-for-mapped* + Defer spawning the client application until the window has been + mapped. See *wait-for-mapped* in *foot.ini*(5). + *--pty* Display an existing pty instead of creating one. This is useful for interacting with VM consoles. diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 66daaea9..89671ba9 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -43,6 +43,14 @@ empty string to be set, but it must be quoted: *KEY=""* Boolean. If enabled, the shell will be launched as a login shell, by prepending a '-' to argv[0]. Default: _no_. +*wait-for-mapped* + Boolean. If enabled, defer spawning the client application until + the window has been mapped and the first frame has been + displayed. The client's first *TIOCGWINSZ* will then return the + final window size, working around startup issues in TUI + applications that mishandle early *SIGWINCH* signals. + Default: _no_. + *term* Value to set the environment variable *TERM* to. Default: _@default_terminfo@_ diff --git a/main.c b/main.c index 9db77d0c..73f41823 100644 --- a/main.c +++ b/main.c @@ -88,6 +88,7 @@ print_usage(const char *prog_name) " -m,--maximized start in maximized mode\n" " -F,--fullscreen start in fullscreen mode\n" " -L,--login-shell start shell as a login shell\n" + " --wait-for-mapped defer client spawn until window is mapped\n" " --pty=PATH display an existing PTY instead of creating one\n" " -D,--working-directory=DIR directory to start in (CWD)\n" " -w,--window-size-pixels=WIDTHxHEIGHT initial width and height, in pixels\n" @@ -187,6 +188,7 @@ sanitize_signals(void) enum { PTY_OPTION = CHAR_MAX + 1, TOPLEVEL_TAG_OPTION = CHAR_MAX + 2, + WAIT_FOR_MAPPED_OPTION = CHAR_MAX + 3, }; int @@ -218,6 +220,7 @@ main(int argc, char *const *argv) {"app-id", required_argument, NULL, 'a'}, {"toplevel-tag", required_argument, NULL, TOPLEVEL_TAG_OPTION}, {"login-shell", no_argument, NULL, 'L'}, + {"wait-for-mapped", no_argument, NULL, WAIT_FOR_MAPPED_OPTION}, {"working-directory", required_argument, NULL, 'D'}, {"font", required_argument, NULL, 'f'}, {"window-size-pixels", required_argument, NULL, 'w'}, @@ -280,6 +283,10 @@ main(int argc, char *const *argv) tll_push_back(overrides, xstrdup("login-shell=yes")); break; + case WAIT_FOR_MAPPED_OPTION: + tll_push_back(overrides, xstrdup("wait-for-mapped=yes")); + break; + case 'T': tll_push_back(overrides, xstrjoin("title=", optarg)); break; diff --git a/render.c b/render.c index cf5f969a..0421cdce 100644 --- a/render.c +++ b/render.c @@ -4317,6 +4317,9 @@ frame_callback(void *data, struct wl_callback *wl_callback, uint32_t callback_da wl_callback_destroy(wl_callback); term->window->frame_callback = NULL; + /* --wait-for-mapped: first frame done, on-screen size is final */ + term_spawn_pending(term); + bool grid = term->render.pending.grid; bool csd = term->render.pending.csd; bool search = term->is_searching && term->render.pending.search; diff --git a/terminal.c b/terminal.c index 8eafbcbe..a3f12ce5 100644 --- a/terminal.c +++ b/terminal.c @@ -294,6 +294,7 @@ fdm_ptmx(struct fdm *fdm, int fd, int events, void *data) } xassert(term->interactive_resizing.grid == NULL); + vt_from_slave(term, buf, count); } @@ -1435,16 +1436,38 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, add_utmp_record(conf, reaper, ptmx); if (!pty_path) { - /* Start the slave/client */ - if ((term->slave = slave_spawn( - term->ptmx, argc, term->cwd, argv, envp, &conf->env_vars, - conf->term, conf->shell, conf->login_shell, - &conf->notifications)) == -1) - { - goto err; - } + if (conf->wait_for_mapped) { + /* Deep-copy: server.c frees argv/envp after term_init() returns */ + char **argv_copy = xcalloc(argc + 1, sizeof(char *)); + for (int i = 0; i < argc; i++) + argv_copy[i] = xstrdup(argv[i]); - reaper_add(term->reaper, term->slave, &fdm_client_terminated, term); + char **envp_copy = NULL; + if (envp != NULL) { + size_t n = 0; + while (envp[n] != NULL) n++; + envp_copy = xcalloc(n + 1, sizeof(char *)); + for (size_t i = 0; i < n; i++) + envp_copy[i] = xstrdup(envp[i]); + } + + term->pending_spawn.armed = true; + term->pending_spawn.argc = argc; + term->pending_spawn.argv = argv_copy; + term->pending_spawn.envp = envp_copy; + term->pending_spawn.cwd = cwd != NULL ? xstrdup(cwd) : NULL; + } else { + /* Start the slave/client */ + if ((term->slave = slave_spawn( + term->ptmx, argc, term->cwd, argv, envp, &conf->env_vars, + conf->term, conf->shell, conf->login_shell, + &conf->notifications)) == -1) + { + goto err; + } + + reaper_add(term->reaper, term->slave, &fdm_client_terminated, term); + } } /* Guess scale; we're not mapped yet, so we don't know on which @@ -1517,6 +1540,54 @@ term_window_configured(struct terminal *term) } } +/* free deferred spawn args in --wait-for-mapped */ +static void +pending_spawn_free(struct terminal *term) +{ + for (int i = 0; i < term->pending_spawn.argc; i++) + free(term->pending_spawn.argv[i]); + free(term->pending_spawn.argv); + if (term->pending_spawn.envp != NULL) { + for (char **e = term->pending_spawn.envp; *e != NULL; e++) + free(*e); + free(term->pending_spawn.envp); + } + free(term->pending_spawn.cwd); + + term->pending_spawn.argc = 0; + term->pending_spawn.argv = NULL; + term->pending_spawn.envp = NULL; + term->pending_spawn.cwd = NULL; +} + +/* --wait-for-mapped: spawn the deferred client. Idempotent. */ +void +term_spawn_pending(struct terminal *term) +{ + if (!term->pending_spawn.armed || term->shutdown.in_progress) + return; + + term->pending_spawn.armed = false; + + term->slave = slave_spawn( + term->ptmx, term->pending_spawn.argc, term->pending_spawn.cwd, + term->pending_spawn.argv, + (const char *const *)term->pending_spawn.envp, + &term->conf->env_vars, term->conf->term, term->conf->shell, + term->conf->login_shell, &term->conf->notifications); + + pending_spawn_free(term); + + if (term->slave == -1) { + LOG_ERR("deferred slave_spawn() failed"); + if (!term->conf->hold_at_exit) + term_shutdown(term); + return; + } + + reaper_add(term->reaper, term->slave, &fdm_client_terminated, term); +} + /* * Shutdown logic * @@ -1825,6 +1896,9 @@ term_destroy(struct terminal *term) del_utmp_record(term->conf, term->reaper, term->ptmx); + /* Frees deferred spawn args if --wait-for-mapped never spawned; else no-op */ + pending_spawn_free(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); diff --git a/terminal.h b/terminal.h index 446d5f23..5e2480a6 100644 --- a/terminal.h +++ b/terminal.h @@ -419,6 +419,15 @@ struct terminal { pid_t slave; int ptmx; + /* --wait-for-mapped: deferred slave_spawn() args (#453) */ + struct { + bool armed; + int argc; + char **argv; + char **envp; + char *cwd; + } pending_spawn; + struct vt vt; struct grid *grid; struct grid normal; @@ -868,6 +877,7 @@ int term_pt_or_px_as_pixels( void term_window_configured(struct terminal *term); +void term_spawn_pending(struct terminal *term); void term_damage_rows(struct terminal *term, int start, int end); void term_damage_rows_in_view(struct terminal *term, int start, int end); diff --git a/tests/test-config.c b/tests/test-config.c index 16cfb2b0..21d93c9d 100644 --- a/tests/test-config.c +++ b/tests/test-config.c @@ -488,6 +488,7 @@ test_section_main(void) test_c32string(&ctx, &parse_section_main, "word-delimiters", &conf.word_delimiters); test_boolean(&ctx, &parse_section_main, "login-shell", &conf.login_shell); + test_boolean(&ctx, &parse_section_main, "wait-for-mapped", &conf.wait_for_mapped); test_boolean(&ctx, &parse_section_main, "box-drawings-uses-font-glyphs", &conf.box_drawings_uses_font_glyphs); test_boolean(&ctx, &parse_section_main, "locked-title", &conf.locked_title); test_boolean(&ctx, &parse_section_main, "dpi-aware", &conf.dpi_aware);