dte test coverage


Directory: ./
File: src/exec.c
Date: 2025-07-03 15:44:24
Exec Total Coverage
Lines: 106 284 37.3%
Functions: 8 13 61.5%
Branches: 46 156 29.5%

Line Branch Exec Source
1 #include "feature.h"
2 #include <fcntl.h>
3 #include <stdint.h>
4 #include <stdlib.h>
5 #include <string.h>
6 #include <sys/mman.h>
7 #include <unistd.h>
8 #include "exec.h"
9 #include "block-iter.h"
10 #include "buffer.h"
11 #include "change.h"
12 #include "command/macro.h"
13 #include "commands.h"
14 #include "config.h"
15 #include "ctags.h"
16 #include "move.h"
17 #include "msg.h"
18 #include "selection.h"
19 #include "tag.h"
20 #include "terminal/mode.h"
21 #include "util/bsearch.h"
22 #include "util/debug.h"
23 #include "util/log.h"
24 #include "util/numtostr.h"
25 #include "util/str-util.h"
26 #include "util/string-view.h"
27 #include "util/string.h"
28 #include "util/strtonum.h"
29 #include "util/xreadwrite.h"
30 #include "util/xsnprintf.h"
31 #include "view.h"
32 #include "window.h"
33
34 enum {
35 IN = 1 << 0,
36 OUT = 1 << 1,
37 ERR = 1 << 2,
38 ALL = IN | OUT | ERR,
39 };
40
41 static const struct {
42 char name[11];
43 uint8_t flags;
44 } exec_map[] = {
45 [EXEC_BUFFER] = {"buffer", IN | OUT},
46 [EXEC_COMMAND] = {"command", IN},
47 [EXEC_ECHO] = {"echo", OUT},
48 [EXEC_ERRMSG] = {"errmsg", ERR},
49 [EXEC_EVAL] = {"eval", OUT},
50 [EXEC_LINE] = {"line", IN},
51 [EXEC_MSG] = {"msg", IN | OUT},
52 [EXEC_MSG_A] = {"msgA", IN | OUT},
53 [EXEC_MSG_B] = {"msgB", IN | OUT},
54 [EXEC_MSG_C] = {"msgC", IN | OUT},
55 [EXEC_NULL] = {"null", ALL},
56 [EXEC_OPEN] = {"open", IN | OUT},
57 [EXEC_OPEN_REL] = {"open-rel", IN},
58 [EXEC_SEARCH] = {"search", IN},
59 [EXEC_TAG] = {"tag", OUT},
60 [EXEC_TAG_A] = {"tagA", OUT},
61 [EXEC_TAG_B] = {"tagB", OUT},
62 [EXEC_TAG_C] = {"tagC", OUT},
63 [EXEC_TTY] = {"tty", ALL},
64 [EXEC_WORD] = {"word", IN},
65 };
66
67 24 UNITTEST {
68 24 CHECK_BSEARCH_ARRAY(exec_map, name);
69 24 }
70
71 18 ExecAction lookup_exec_action(const char *name, int fd)
72 {
73 18 BUG_ON(fd < 0 || fd > 2);
74 18 ssize_t i = BSEARCH_IDX(name, exec_map, vstrcmp);
75
3/4
✓ Branch 0 (5→6) taken 15 times.
✓ Branch 1 (5→8) taken 3 times.
✓ Branch 2 (6→7) taken 15 times.
✗ Branch 3 (6→8) not taken.
18 return (i >= 0 && (exec_map[i].flags & 1u << fd)) ? i : EXEC_INVALID;
76 }
77
78 1 void collect_exec_actions(PointerArray *a, const char *prefix, int fd)
79 {
80
1/2
✓ Branch 0 (2→3) taken 1 times.
✗ Branch 1 (2→10) not taken.
1 if (unlikely(fd < 0 || fd > 2)) {
81 return;
82 }
83
84 1 size_t prefix_len = strlen(prefix);
85 1 unsigned int flag = 1u << fd;
86
2/2
✓ Branch 0 (9→4) taken 20 times.
✓ Branch 1 (9→10) taken 1 times.
21 for (size_t i = 0; i < ARRAYLEN(exec_map); i++) {
87 20 const char *action = exec_map[i].name;
88
3/4
✓ Branch 0 (4→5) taken 14 times.
✓ Branch 1 (4→8) taken 6 times.
✓ Branch 2 (5→6) taken 14 times.
✗ Branch 3 (5→8) not taken.
20 if ((exec_map[i].flags & flag) && str_has_strn_prefix(action, prefix, prefix_len)) {
89 14 ptr_array_append(a, xstrdup(action));
90 }
91 }
92 }
93
94 static void open_files_from_string(EditorState *e, const String *str)
95 {
96 PointerArray filenames = PTR_ARRAY_INIT;
97 for (size_t pos = 0, size = str->len; pos < size; ) {
98 char *filename = buf_next_line(str->buffer, &pos, size);
99 if (filename[0] != '\0') {
100 ptr_array_append(&filenames, filename);
101 }
102 }
103
104 if (filenames.count == 0) {
105 return;
106 }
107
108 ptr_array_append(&filenames, NULL);
109 window_open_files(e->window, (char**)filenames.ptrs, NULL);
110
111 // TODO: re-enable this when the todo in allow_macro_recording() is done
112 // macro_command_hook(&e->macro, "open", (char**)filenames.ptrs);
113
114 ptr_array_free_array(&filenames);
115 }
116
117 static void parse_and_activate_message(EditorState *e, const String *str, ExecAction action)
118 {
119 if (unlikely(str->len == 0)) {
120 error_msg(&e->err, "child produced no output");
121 return;
122 }
123
124 size_t msgs_idx = (action == EXEC_MSG) ? 0 : action - EXEC_MSG_A;
125 BUG_ON(msgs_idx >= ARRAYLEN(e->messages));
126 MessageList *msgs = &e->messages[msgs_idx];
127 size_t count = msgs->array.count;
128 size_t x;
129
130 if (!count || !buf_parse_size(str->buffer, str->len, &x) || !x) {
131 return;
132 }
133
134 msgs->pos = MIN(x - 1, count - 1);
135 activate_current_message(msgs, e->window);
136 }
137
138 static void parse_and_activate_tags(EditorState *e, const String *str, ExecAction action)
139 {
140 ErrorBuffer *ebuf = &e->err;
141 if (unlikely(str->len == 0)) {
142 error_msg(ebuf, "child produced no output");
143 return;
144 }
145
146 char cwd[8192];
147 if (unlikely(!getcwd(cwd, sizeof cwd))) {
148 error_msg_errno(ebuf, "getcwd() failed");
149 return;
150 }
151
152 const StringView dir = strview_from_cstring(cwd);
153 const char *buffer_filename = e->buffer->abs_filename;
154 TagFile *tf = &e->tagfile;
155 enum {NOT_LOADED, LOADED, FAILED} tf_status = NOT_LOADED;
156
157 size_t msgs_idx = (action == EXEC_TAG) ? e->options.msg_tag : action - EXEC_TAG_A;
158 BUG_ON(msgs_idx >= ARRAYLEN(e->messages));
159 MessageList *msgs = &e->messages[msgs_idx];
160 clear_messages(msgs);
161
162 for (size_t pos = 0, len = str->len; pos < len; ) {
163 Tag tag;
164 StringView line = buf_slice_next_line(str->buffer, &pos, len);
165 if (line.length == 0) {
166 continue;
167 }
168
169 bool parsed = parse_ctags_line(&tag, line.data, line.length);
170 if (parsed) {
171 // `line` is a valid tags(5) file entry; handle it directly
172 add_message_for_tag(msgs, &tag, &dir);
173 continue;
174 }
175
176 // Treat `line` as a simple tag name (look it up in the tags(5) file)
177 switch (tf_status) {
178 case NOT_LOADED:
179 tf_status = load_tag_file(tf, ebuf) ? LOADED : FAILED;
180 if (tf_status == FAILED) {
181 continue;
182 }
183 // Fallthrough
184 case LOADED:
185 tag_lookup(tf, msgs, ebuf, &line, buffer_filename);
186 continue;
187 case FAILED:
188 continue;
189 }
190 }
191
192 activate_current_message_save(msgs, &e->bookmarks, e->view);
193 }
194
195 static void insert_to_selection (
196 View *view,
197 const String *output,
198 const SelectionInfo *info
199 ) {
200 size_t del_count = info->eo - info->so;
201 buffer_replace_bytes(view, del_count, output->buffer, output->len);
202
203 if (output->len == 0) {
204 // If the selection was replaced with 0 bytes then there's nothing
205 // new to select, so just unselect instead
206 unselect(view);
207 return;
208 }
209
210 // Keep the selection and adjust the size to the newly inserted text
211 size_t so = info->so;
212 size_t eo = so + (output->len - 1);
213 block_iter_goto_offset(&view->cursor, info->swapped ? so : eo);
214 view->sel_so = info->swapped ? eo : so;
215 view->sel_eo = SEL_EO_RECALC;
216 }
217
218 4 static void show_spawn_error_msg(ErrorBuffer *ebuf, const String *errstr, int err)
219 {
220
2/2
✓ Branch 0 (2→3) taken 1 times.
✓ Branch 1 (2→4) taken 3 times.
4 if (err <= 0) {
221 // spawn() returned an error code instead of an exit code, which
222 // indicates that something failed before (or during) the child
223 // process exec(3p), or that an error occurred in wait_child().
224 // In this case, error_msg() has already been called.
225 1 return;
226 }
227
228 3 char msg[512];
229 3 msg[0] = '\0';
230
2/2
✓ Branch 0 (4→5) taken 1 times.
✓ Branch 1 (4→10) taken 2 times.
3 if (errstr->len) {
231 1 size_t pos = 0;
232 1 StringView line = buf_slice_next_line(errstr->buffer, &pos, errstr->len);
233 1 BUG_ON(pos == 0);
234 1 size_t len = MIN(line.length, sizeof(msg) - 8);
235 1 xsnprintf(msg, sizeof(msg), ": \"%.*s\"", (int)len, line.data);
236 }
237
238
2/2
✓ Branch 0 (10→11) taken 1 times.
✓ Branch 1 (10→15) taken 2 times.
3 if (err >= 256) {
239 1 int sig = err >> 8;
240 1 const char *str = strsignal(sig);
241
1/2
✗ Branch 0 (12→13) not taken.
✓ Branch 1 (12→14) taken 1 times.
1 error_msg(ebuf, "Child received signal %d (%s)%s", sig, str ? str : "??", msg);
242 2 } else if (err) {
243 2 error_msg(ebuf, "Child returned %d%s", err, msg);
244 }
245 }
246
247 45 static SpawnAction spawn_action_from_exec_action(ExecAction action)
248 {
249 45 BUG_ON(action == EXEC_INVALID);
250
1/2
✓ Branch 0 (4→5) taken 45 times.
✗ Branch 1 (4→7) not taken.
45 if (action == EXEC_NULL) {
251 return SPAWN_NULL;
252
2/2
✓ Branch 0 (5→6) taken 15 times.
✓ Branch 1 (5→7) taken 30 times.
45 } else if (action == EXEC_TTY) {
253 return SPAWN_TTY;
254 } else {
255 15 return SPAWN_PIPE;
256 }
257 }
258
259 int open_builtin_script(ErrorBuffer *ebuf, const char *name)
260 {
261 const BuiltinConfig *cfg = get_builtin_config(name);
262 if (unlikely(!cfg)) {
263 error_msg(ebuf, "no built-in script with name '%s'", name);
264 return -1;
265 }
266 if (unlikely(!str_has_prefix(name, "script/"))) {
267 error_msg(ebuf, "built-in config '%s' not an executable script", name);
268 return -1;
269 }
270
271 #if HAVE_MEMFD_CREATE && HAVE_FEXECVE
272 // MFD_CLOEXEC isn't used here, due to a bug in the way fexecve(3)
273 // is implemented on Linux. See also:
274 // • https://man7.org/linux/man-pages/man3/fexecve.3.html#BUGS
275 // • https://man7.org/linux/man-pages/man2/execveat.2.html#BUGS
276 int prog_fd = memfd_create(name, MFD_ALLOW_SEALING);
277 if (prog_fd < 0) {
278 error_msg_errno(ebuf, "memfd_create");
279 return -1;
280 }
281
282 if (xwrite_all(prog_fd, cfg->text.data, cfg->text.length) < 0) {
283 error_msg_errno(ebuf, "xwrite_all");
284 xclose(prog_fd);
285 return -1;
286 }
287
288 int seals = F_SEAL_SEAL | F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_WRITE;
289 if (fcntl(prog_fd, F_ADD_SEALS, seals)) {
290 // Not a hard error, since sealing isn't strictly needed
291 LOG_ERRNO("setting file seals failed");
292 }
293
294 return prog_fd;
295 #endif
296
297 // TODO: Use shm_open() as a fallback implementation. That API is
298 // inferior for this use case, but it's part of the POSIX [SHM]
299 // option and is also more portable in practice (e.g. to OpenBSD,
300 // macOS, etc.).
301 error_msg(ebuf, "executing built-in scripts not yet supported on this platform");
302 return -1;
303 }
304
305 // NOLINTNEXTLINE(readability-function-size)
306 15 ssize_t handle_exec (
307 EditorState *e,
308 const char **argv,
309 ExecAction actions[3],
310 ExecFlags exec_flags
311 ) {
312 15 View *view = e->view;
313 15 const BlockIter saved_cursor = view->cursor;
314 15 const ssize_t saved_sel_so = view->sel_so;
315 15 const ssize_t saved_sel_eo = view->sel_eo;
316 15 char *alloc = NULL;
317 15 bool output_to_buffer = (actions[STDOUT_FILENO] == EXEC_BUFFER);
318 15 bool input_from_buffer = false;
319 15 bool replace_unselected_input = false;
320 15 bool quiet = (exec_flags & EXECFLAG_QUIET);
321 15 int prog_fd = -1;
322
323
1/2
✗ Branch 0 (2→3) not taken.
✓ Branch 1 (2→5) taken 15 times.
15 if (exec_flags & EXECFLAG_BUILTIN) {
324 prog_fd = open_builtin_script(&e->err, argv[0]);
325 if (prog_fd < 0) {
326 return -1;
327 }
328 }
329
330 80 SpawnContext ctx = {
331 .argv = argv,
332 .prog_fd = prog_fd,
333 .outputs = {STRING_INIT, STRING_INIT},
334 .quiet = quiet,
335 15 .ebuf = &e->err,
336
2/2
✓ Branch 0 (8→9) taken 5 times.
✓ Branch 1 (8→11) taken 10 times.
15 .lines = output_to_buffer ? view->window->edit_h : 0,
337
1/2
✓ Branch 0 (9→10) taken 5 times.
✗ Branch 1 (9→11) not taken.
5 .columns = output_to_buffer ? view->window->edit_w : 0,
338 .actions = {
339 15 spawn_action_from_exec_action(actions[0]),
340 15 spawn_action_from_exec_action(actions[1]),
341 15 spawn_action_from_exec_action(actions[2]),
342 },
343 };
344
345 15 ExecAction in_action = actions[STDIN_FILENO];
346
2/10
✗ Branch 0 (11→12) not taken.
✓ Branch 1 (11→16) taken 3 times.
✗ Branch 2 (11→21) not taken.
✗ Branch 3 (11→29) not taken.
✗ Branch 4 (11→34) not taken.
✗ Branch 5 (11→36) not taken.
✗ Branch 6 (11→38) not taken.
✗ Branch 7 (11→40) not taken.
✗ Branch 8 (11→42) not taken.
✓ Branch 9 (11→43) taken 12 times.
15 switch (in_action) {
347 case EXEC_LINE:
348 input_from_buffer = true;
349 if (!view->selection) {
350 move_bol(view, BOL_SIMPLE);
351 StringView line = block_iter_get_line(&view->cursor);
352 ctx.input.length = line.length;
353 replace_unselected_input = true;
354 }
355 break;
356 3 case EXEC_BUFFER:
357 3 input_from_buffer = true;
358
1/2
✓ Branch 0 (16→17) taken 3 times.
✗ Branch 1 (16→43) not taken.
3 if (!view->selection) {
359 3 const Block *blk;
360
2/2
✓ Branch 0 (19→18) taken 3 times.
✓ Branch 1 (19→20) taken 3 times.
6 block_for_each(blk, &view->buffer->blocks) {
361 3 ctx.input.length += blk->size;
362 }
363 3 move_bof(view);
364 3 replace_unselected_input = true;
365 }
366 break;
367 case EXEC_WORD:
368 input_from_buffer = true;
369 if (!view->selection) {
370 StringView line;
371 size_t offset = get_current_line_and_offset(&view->cursor, &line);
372 size_t start = offset;
373 size_t end = get_bounds_for_word_under_cursor(line, &start);
374 if (end == 0) {
375 break;
376 }
377
378 // If `start` is less than `offset` here, the subtraction wraps
379 // but nevertheless works as intended
380 view->cursor.offset += start - offset; // == view->cursor.offset -= (offset - start)
381
382 ctx.input.length = end - start;
383 BUG_ON(view->cursor.offset >= view->cursor.blk->size);
384 replace_unselected_input = true;
385 }
386 break;
387 case EXEC_MSG:
388 case EXEC_MSG_A:
389 case EXEC_MSG_B:
390 case EXEC_MSG_C: {
391 size_t msgs_idx = (in_action == EXEC_MSG) ? 0 : in_action - EXEC_MSG_A;
392 BUG_ON(msgs_idx >= ARRAYLEN(e->messages));
393 String messages = dump_messages(&e->messages[msgs_idx]);
394 ctx.input = strview_from_string(&messages);
395 alloc = messages.buffer;
396 break;
397 }
398 case EXEC_COMMAND: {
399 String hist = history_dump(&e->command_history);
400 ctx.input = strview_from_string(&hist);
401 alloc = hist.buffer;
402 break;
403 }
404 case EXEC_SEARCH: {
405 String hist = history_dump(&e->search_history);
406 ctx.input = strview_from_string(&hist);
407 alloc = hist.buffer;
408 break;
409 }
410 case EXEC_OPEN: {
411 String hist = file_history_dump(&e->file_history);
412 ctx.input = strview_from_string(&hist);
413 alloc = hist.buffer;
414 break;
415 }
416 case EXEC_OPEN_REL: {
417 String hist = file_history_dump_relative(&e->file_history);
418 ctx.input = strview_from_string(&hist);
419 alloc = hist.buffer;
420 break;
421 }
422 case EXEC_NULL:
423 case EXEC_TTY:
424 break;
425 // These can't be used as input actions and should be prevented by
426 // the validity checks in cmd_exec():
427 case EXEC_TAG:
428 case EXEC_TAG_A:
429 case EXEC_TAG_B:
430 case EXEC_TAG_C:
431 case EXEC_ECHO:
432 case EXEC_EVAL:
433 case EXEC_ERRMSG:
434 case EXEC_INVALID:
435 default:
436 BUG("unhandled action");
437 return -1;
438 }
439
440 // This could be left uninitialized, but doing so makes some old compilers
441 // produce false-positive "-Wmaybe-uninitialized" warnings
442 15 SelectionInfo info = {.si = view->cursor};
443
444
1/4
✗ Branch 0 (43→44) not taken.
✓ Branch 1 (43→48) taken 15 times.
✗ Branch 2 (44→45) not taken.
✗ Branch 3 (44→51) not taken.
15 if (view->selection && (input_from_buffer || output_to_buffer)) {
445 info = init_selection(view);
446 view->cursor = info.si;
447 if (input_from_buffer) {
448 ctx.input.length = info.eo - info.so;
449 }
450 }
451
452
2/2
✓ Branch 0 (48→49) taken 3 times.
✓ Branch 1 (48→51) taken 12 times.
15 if (input_from_buffer) {
453 3 alloc = block_iter_get_bytes(&view->cursor, ctx.input.length);
454 3 ctx.input.data = alloc;
455 }
456
457 15 yield_terminal(e, quiet);
458 15 int err = spawn(&ctx);
459 15 xclose(prog_fd);
460
3/4
✓ Branch 0 (54→55) taken 14 times.
✓ Branch 1 (54→56) taken 1 times.
✓ Branch 2 (55→56) taken 14 times.
✗ Branch 3 (55→57) not taken.
15 bool prompt = (err >= 0) && (exec_flags & EXECFLAG_PROMPT);
461 15 resume_terminal(e, quiet, prompt);
462 15 free(alloc);
463
464
2/2
✓ Branch 0 (58→59) taken 4 times.
✓ Branch 1 (58→63) taken 11 times.
15 if (err != 0) {
465 4 show_spawn_error_msg(&e->err, &ctx.outputs[1], err);
466 4 string_free(&ctx.outputs[0]);
467 4 string_free(&ctx.outputs[1]);
468 4 view->cursor = saved_cursor;
469 4 return -1;
470 }
471
472 11 string_free(&ctx.outputs[1]);
473 11 String *output = &ctx.outputs[0];
474 11 bool strip_trailing_newline = (exec_flags & EXECFLAG_STRIP_NL);
475 11 if (
476 strip_trailing_newline
477
1/2
✗ Branch 0 (64→65) not taken.
✓ Branch 1 (64→70) taken 11 times.
11 && output_to_buffer
478 && output->len > 0
479 && output->buffer[output->len - 1] == '\n'
480 ) {
481 output->len--;
482 if (output->len > 0 && output->buffer[output->len - 1] == '\r') {
483 output->len--;
484 }
485 }
486
487
2/2
✓ Branch 0 (70→71) taken 6 times.
✓ Branch 1 (70→72) taken 5 times.
11 if (!output_to_buffer) {
488 6 view->cursor = saved_cursor;
489 6 view->sel_so = saved_sel_so;
490 6 view->sel_eo = saved_sel_eo;
491 6 mark_all_lines_changed(view->buffer);
492 }
493
494 11 ExecAction out_action = actions[STDOUT_FILENO];
495
2/8
✓ Branch 0 (72→73) taken 5 times.
✗ Branch 1 (72→78) not taken.
✗ Branch 2 (72→82) not taken.
✗ Branch 3 (72→83) not taken.
✗ Branch 4 (72→85) not taken.
✗ Branch 5 (72→87) not taken.
✗ Branch 6 (72→89) not taken.
✓ Branch 7 (72→91) taken 6 times.
11 switch (out_action) {
496 5 case EXEC_BUFFER:
497
1/2
✗ Branch 0 (73→74) not taken.
✓ Branch 1 (73→75) taken 5 times.
5 if (view->selection) {
498 insert_to_selection(view, output, &info);
499 } else {
500
2/2
✓ Branch 0 (75→76) taken 2 times.
✓ Branch 1 (75→77) taken 3 times.
5 size_t del_count = replace_unselected_input ? ctx.input.length : 0;
501 5 buffer_replace_bytes(view, del_count, output->buffer, output->len);
502 }
503 break;
504 case EXEC_ECHO:
505 if (output->len) {
506 size_t pos = 0;
507 StringView line = buf_slice_next_line(output->buffer, &pos, output->len);
508 info_msg(&e->err, "%.*s", (int)line.length, line.data);
509 }
510 break;
511 case EXEC_MSG:
512 case EXEC_MSG_A:
513 case EXEC_MSG_B:
514 case EXEC_MSG_C:
515 parse_and_activate_message(e, output, out_action);
516 break;
517 case EXEC_OPEN:
518 open_files_from_string(e, output);
519 break;
520 case EXEC_TAG:
521 case EXEC_TAG_A:
522 case EXEC_TAG_B:
523 case EXEC_TAG_C:
524 parse_and_activate_tags(e, output, out_action);
525 break;
526 case EXEC_EVAL:
527 exec_normal_config(e, strview_from_string(output));
528 break;
529 case EXEC_NULL:
530 case EXEC_TTY:
531 break;
532 // These can't be used as output actions
533 case EXEC_COMMAND:
534 case EXEC_ERRMSG:
535 case EXEC_LINE:
536 case EXEC_OPEN_REL:
537 case EXEC_SEARCH:
538 case EXEC_WORD:
539 case EXEC_INVALID:
540 default:
541 BUG("unhandled action");
542 return -1;
543 }
544
545 11 size_t output_len = output->len;
546 11 string_free(output);
547 11 return output_len;
548 }
549
550 15 void yield_terminal(EditorState *e, bool quiet)
551 {
552
1/2
✗ Branch 0 (2→3) not taken.
✓ Branch 1 (2→7) taken 15 times.
15 if (e->flags & EFLAG_HEADLESS) {
553 return;
554 }
555
556 if (quiet) {
557 term_raw_isig();
558 } else {
559 e->child_controls_terminal = true;
560 ui_end(e, false);
561 term_cooked();
562 }
563 }
564
565 15 void resume_terminal(EditorState *e, bool quiet, bool prompt)
566 {
567
1/2
✗ Branch 0 (2→3) not taken.
✓ Branch 1 (2→10) taken 15 times.
15 if (e->flags & EFLAG_HEADLESS) {
568 return;
569 }
570
571 term_raw();
572 if (!quiet && e->child_controls_terminal) {
573 if (prompt) {
574 any_key(&e->terminal, e->options.esc_timeout);
575 }
576 ui_start(e);
577 e->child_controls_terminal = false;
578 }
579 }
580