A fast, lightweight and minimalistic Wayland terminal emulator
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

1842 lines
51 KiB

#include "terminal.h"
#include <malloc.h>
#include <string.h>
#include <unistd.h>
#include <assert.h>
#include <errno.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/epoll.h>
#include <sys/eventfd.h>
#include <sys/timerfd.h>
#include <fcntl.h>
#include <linux/input-event-codes.h>
#define LOG_MODULE "terminal"
#define LOG_ENABLE_DBG 0
#include "log.h"
#include "async.h"
#include "grid.h"
#include "render.h"
#include "vt.h"
#include "selection.h"
#include "config.h"
#include "slave.h"
#define min(x, y) ((x) < (y) ? (x) : (y))
#define max(x, y) ((x) > (y) ? (x) : (y))
static const char *const XCURSOR_LEFT_PTR = "left_ptr";
static const char *const XCURSOR_TEXT = "text";
static const char *const XCURSOR_HAND2 = "hand2";
bool
term_to_slave(struct terminal *term, const void *_data, size_t len)
{
size_t async_idx = 0;
if (tll_length(term->ptmx_buffer) > 0) {
/* With a non-empty queue, EPOLLOUT has already been enabled */
goto enqueue_data;
}
/*
* Try a synchronous write first. If we fail to write everything,
* switch to asynchronous.
*/
switch (async_write(term->ptmx, _data, len, &async_idx)) {
case ASYNC_WRITE_REMAIN:
/* Switch to asynchronous mode; let FDM write the remaining data */
if (!fdm_event_add(term->fdm, term->ptmx, EPOLLOUT))
return false;
goto enqueue_data;
case ASYNC_WRITE_DONE:
return true;
case ASYNC_WRITE_ERR:
LOG_ERRNO("failed to synchronously write %zu bytes to slave", len);
return false;
}
/* Shouldn't get here */
assert(false);
return false;
enqueue_data:
/*
* We're in asynchronous mode - push data to queue and let the FDM
* handler take care of it
*/
{
void *copy = malloc(len);
memcpy(copy, _data, len);
struct ptmx_buffer queued = {
.data = copy,
.len = len,
.idx = async_idx,
};
tll_push_back(term->ptmx_buffer, queued);
}
return true;
}
static bool
fdm_ptmx_out(struct fdm *fdm, int fd, int events, void *data)
{
struct terminal *term = data;
/* If there is no queued data, then we shouldn't be in asynchronous mode */
assert(tll_length(term->ptmx_buffer) > 0);
/* Don't use pop() since we may not be able to write the entire buffer */
tll_foreach(term->ptmx_buffer, it) {
switch (async_write(term->ptmx, it->item.data, it->item.len, &it->item.idx)) {
case ASYNC_WRITE_DONE:
free(it->item.data);
tll_remove(term->ptmx_buffer, it);
break;
case ASYNC_WRITE_REMAIN:
/* to_slave() updated it->item.idx */
return true;
case ASYNC_WRITE_ERR:
LOG_ERRNO("failed to asynchronously write %zu bytes to slave",
it->item.len - it->item.idx);
return false;
}
}
/* No more queued data, switch back to synchronous mode */
fdm_event_del(term->fdm, term->ptmx, EPOLLOUT);
return true;
}
#define PTMX_TIMING 0
#if PTMX_TIMING
static struct timespec last = {0};
#endif
static bool
fdm_ptmx(struct fdm *fdm, int fd, int events, void *data)
{
struct terminal *term = data;
bool pollin = events & EPOLLIN;
bool pollout = events & EPOLLOUT;
bool hup = events & EPOLLHUP;
if (hup) {
/* TODO: should we *not* ignore pollin? */
return term_shutdown(term);
}
if (pollout) {
if (!fdm_ptmx_out(fdm, fd, events, data))
return false;
}
if (!pollin)
return true;
uint8_t buf[24 * 1024];
ssize_t count = read(term->ptmx, buf, sizeof(buf));
if (count < 0) {
LOG_ERRNO("failed to read from pseudo terminal");
return false;
}
vt_from_slave(term, buf, count);
/* Prevent blinking while typing */
term_cursor_blink_restart(term);
/*
* We likely need to re-render. But, we don't want to
* do it immediately. Often, a single client operation
* is done through multiple writes. Many times, we're
* so fast that we render mid-operation frames.
*
* For example, we might end up rendering a frame
* where the client just erased a line, while in the
* next frame, the client wrote to the same line. This
* causes screen "flashes".
*
* Mitigate by always incuring a small delay before
* rendering the next frame. This gives the client
* some time to finish the operation (and thus gives
* us time to receive the last writes before doing any
* actual rendering).
*
* We incur this delay *every* time we receive
* input. To ensure we don't delay rendering
* indefinitely, we start a second timer that is only
* reset when we render.
*
* Note that when the client is producing data at a
* very high pace, we're rate limited by the wayland
* compositor anyway. The delay we introduce here only
* has any effect when the renderer is idle.
*/
if (term->window->frame_callback == NULL) {
if (term->render.app_sync_updates.enabled)
term->render.refresh_needed = true;
else {
/* First timeout - reset each time we receive input. */
#if PTMX_TIMING
struct timespec now;
clock_gettime(1, &now);
if (last.tv_sec > 0 || last.tv_nsec > 0) {
struct timeval diff;
struct timeval l = {last.tv_sec, last.tv_nsec / 1000};
struct timeval n = {now.tv_sec, now.tv_nsec / 1000};
timersub(&n, &l, &diff);
LOG_INFO("waited %lu µs for more input", diff.tv_usec);
}
last = now;
#endif
timerfd_settime(
term->delayed_render_timer.lower_fd, 0,
&(struct itimerspec){.it_value = {.tv_nsec = 500000}},
NULL);
/* Second timeout - only reset when we render. Set to one
* frame (assuming 60Hz) */
if (!term->delayed_render_timer.is_armed) {
timerfd_settime(
term->delayed_render_timer.upper_fd, 0,
&(struct itimerspec){.it_value = {.tv_nsec = 16666666 / 2}},
NULL);
term->delayed_render_timer.is_armed = true;
}
}
} else
term->render.pending = true;
if (events & EPOLLHUP)
return term_shutdown(term);
return true;
}
static bool
fdm_flash(struct fdm *fdm, int fd, int events, void *data)
{
if (events & EPOLLHUP)
return false;
struct terminal *term = data;
uint64_t expiration_count;
ssize_t ret = read(
term->flash.fd, &expiration_count, sizeof(expiration_count));
if (ret < 0) {
if (errno == EAGAIN)
return true;
LOG_ERRNO("failed to read flash timer");
return false;
}
LOG_DBG("flash timer expired %llu times",
(unsigned long long)expiration_count);
term->flash.active = false;
term_damage_view(term);
render_refresh(term);
return true;
}
static bool
fdm_blink(struct fdm *fdm, int fd, int events, void *data)
{
if (events & EPOLLHUP)
return false;
struct terminal *term = data;
uint64_t expiration_count;
ssize_t ret = read(
term->blink.fd, &expiration_count, sizeof(expiration_count));
if (ret < 0) {
if (errno == EAGAIN)
return true;
LOG_ERRNO("failed to read blink timer");
return false;
}
LOG_DBG("blink timer expired %llu times",
(unsigned long long)expiration_count);
/* Invert blink state */
term->blink.state = term->blink.state == BLINK_ON
? BLINK_OFF : BLINK_ON;
/* Scan all visible cells and mark rows with blinking cells dirty */
bool no_blinking_cells = true;
for (int r = 0; r < term->rows; r++) {
struct row *row = grid_row_in_view(term->grid, r);
for (int col = 0; col < term->cols; col++) {
struct cell *cell = &row->cells[col];
if (cell->attrs.blink) {
cell->attrs.clean = 0;
row->dirty = true;
no_blinking_cells = false;
}
}
}
if (no_blinking_cells) {
LOG_DBG("disarming blink timer");
term->blink.active = false;
term->blink.state = BLINK_ON;
static const struct itimerspec disarm = {{0}};
if (timerfd_settime(term->blink.fd, 0, &disarm, NULL) < 0)
LOG_ERRNO("failed to disarm blink timer");
} else
render_refresh(term);
return true;
}
void
term_arm_blink_timer(struct terminal *term)
{
if (term->blink.active)
return;
LOG_DBG("arming blink timer");
struct itimerspec alarm = {
.it_value = {.tv_sec = 0, .tv_nsec = 500 * 1000000},
.it_interval = {.tv_sec = 0, .tv_nsec = 500 * 1000000},
};
if (timerfd_settime(term->blink.fd, 0, &alarm, NULL) < 0)
LOG_ERRNO("failed to arm blink timer");
else
term->blink.active = true;
}
static void
cursor_refresh(struct terminal *term)
{
term->grid->cur_row->cells[term->cursor.point.col].attrs.clean = 0;
term->grid->cur_row->dirty = true;
render_refresh(term);
}
static bool
fdm_cursor_blink(struct fdm *fdm, int fd, int events, void *data)
{
if (events & EPOLLHUP)
return false;
struct terminal *term = data;
uint64_t expiration_count;
ssize_t ret = read(
term->cursor_blink.fd, &expiration_count, sizeof(expiration_count));
if (ret < 0) {
if (errno == EAGAIN)
return true;
LOG_ERRNO("failed to read cursor blink timer");
return false;
}
LOG_DBG("cursor blink timer expired %llu times",
(unsigned long long)expiration_count);
/* Invert blink state */
term->cursor_blink.state = term->cursor_blink.state == CURSOR_BLINK_ON
? CURSOR_BLINK_OFF : CURSOR_BLINK_ON;
cursor_refresh(term);
return true;
}
static bool
fdm_delayed_render(struct fdm *fdm, int fd, int events, void *data)
{
if (events & EPOLLHUP)
return false;
struct terminal *term = data;
if (!term->delayed_render_timer.is_armed)
return true;
uint64_t unused;
ssize_t ret1 = 0;
ssize_t ret2 = 0;
if (fd == term->delayed_render_timer.lower_fd)
ret1 = read(term->delayed_render_timer.lower_fd, &unused, sizeof(unused));
if (fd == term->delayed_render_timer.upper_fd)
ret2 = read(term->delayed_render_timer.upper_fd, &unused, sizeof(unused));
if ((ret1 < 0 || ret2 < 0)) {
if (errno == EAGAIN)
return true;
LOG_ERRNO("failed to read timeout timer");
return false;
}
if (ret1 > 0)
LOG_DBG("lower delay timer expired");
else if (ret2 > 0)
LOG_DBG("upper delay timer expired");
#if PTMX_TIMING
last = (struct timespec){0};
#endif
render_refresh(term);
/* Reset timers */
struct itimerspec reset = {{0}};
timerfd_settime(term->delayed_render_timer.lower_fd, 0, &reset, NULL);
timerfd_settime(term->delayed_render_timer.upper_fd, 0, &reset, NULL);
term->delayed_render_timer.is_armed = false;
return true;
}
static bool
fdm_app_sync_updates_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.app_sync_updates.timer_fd,
&unused, sizeof(unused));
if (ret < 0) {
if (errno == EAGAIN)
return true;
LOG_ERRNO("failed to read application synchronized updates timeout timer");
return false;
}
term_disable_app_sync_updates(term);
return true;
}
static void
initialize_color_cube(struct terminal *term)
{
/* First 16 entries have already been initialized from conf */
for (size_t r = 0; r < 6; r++) {
for (size_t g = 0; g < 6; g++) {
for (size_t b = 0; b < 6; b++) {
term->colors.default_table[16 + r * 6 * 6 + g * 6 + b]
= r * 51 << 16 | g * 51 << 8 | b * 51;
}
}
}
for (size_t i = 0; i < 24; i++)
term->colors.default_table[232 + i] = i * 11 << 16 | i * 11 << 8 | i * 11;
memcpy(term->colors.table, term->colors.default_table, sizeof(term->colors.table));
}
static bool
initialize_render_workers(struct terminal *term)
{
LOG_INFO("using %zu rendering threads", term->render.workers.count);
sem_init(&term->render.workers.start, 0, 0);
sem_init(&term->render.workers.done, 0, 0);
mtx_init(&term->render.workers.lock, mtx_plain);
cnd_init(&term->render.workers.cond);
term->render.workers.threads = calloc(
term->render.workers.count, sizeof(term->render.workers.threads[0]));
for (size_t i = 0; i < term->render.workers.count; i++) {
struct render_worker_context *ctx = malloc(sizeof(*ctx));
*ctx = (struct render_worker_context) {
.term = term,
.my_id = 1 + i,
};
int ret = thrd_create(
&term->render.workers.threads[i], &render_worker_thread, ctx);
if (ret != 0) {
LOG_ERRNO_P("failed to create render worker thread", ret);
term->render.workers.threads[i] = 0;
return false;
}
}
return true;
}
static bool
initialize_fonts(struct terminal *term, const struct config *conf)
{
const size_t count = tll_length(conf->fonts);
const char *names[count];
size_t i = 0;
tll_foreach(conf->fonts, it)
names[i++] = it->item;
/* Use highest DPI available */
unsigned dpi = 96;
tll_foreach(term->wl->monitors, it) {
if (it->item.y_ppi > dpi)
dpi = it->item.y_ppi;
}
char attrs0[64], attrs1[64], attrs2[64], attrs3[64];
snprintf(attrs0, sizeof(attrs0), "dpi=%u", dpi);
snprintf(attrs1, sizeof(attrs1), "dpi=%u:weight=bold", dpi);
snprintf(attrs2, sizeof(attrs2), "dpi=%u:slant=italic", dpi);
snprintf(attrs3, sizeof(attrs3), "dpi=%u:weight=bold:slant=italic", dpi);
return (
(term->fonts[0] = font_from_name(names, count, attrs0)) != NULL &&
(term->fonts[1] = font_from_name(names, count, attrs1)) != NULL &&
(term->fonts[2] = font_from_name(names, count, attrs2)) != NULL &&
(term->fonts[3] = font_from_name(names, count, attrs3)) != NULL);
}
struct terminal *
term_init(const struct config *conf, struct fdm *fdm, struct wayland *wayl,
const char *term_env, const char *foot_exe, const char *cwd,
int argc, char *const *argv,
void (*shutdown_cb)(void *data, int exit_code), void *shutdown_data)
{
int ptmx = -1;
int flash_fd = -1;
int blink_fd = -1;
int cursor_blink_fd = -1;
int delay_lower_fd = -1;
int delay_upper_fd = -1;
int app_sync_updates_fd = -1;
struct terminal *term = malloc(sizeof(*term));
if ((ptmx = posix_openpt(O_RDWR | O_NOCTTY)) == -1) {
LOG_ERRNO("failed to open PTY");
goto close_fds;
}
if ((flash_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) == -1) {
LOG_ERRNO("failed to create flash timer FD");
goto close_fds;
}
if ((blink_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) == -1) {
LOG_ERRNO("failed to create blink timer FD");
goto close_fds;
}
if ((cursor_blink_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) == -1) {
LOG_ERRNO("failed to create cursor blink timer FD");
goto close_fds;
}
if ((delay_lower_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) == -1 ||
(delay_upper_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) == -1)
{
LOG_ERRNO("failed to create delayed rendering timer FDs");
goto close_fds;
}
if ((app_sync_updates_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) == -1)
{
LOG_ERRNO("failed to create application synchronized updates timer FD");
goto close_fds;
}
int ptmx_flags;
if ((ptmx_flags = fcntl(ptmx, F_GETFL)) < 0 ||
fcntl(ptmx, F_SETFL, ptmx_flags | O_NONBLOCK) < 0)
{
LOG_ERRNO("failed to configure ptmx as non-blocking");
goto err;
}
if (!fdm_add(fdm, ptmx, EPOLLIN, &fdm_ptmx, term) ||
!fdm_add(fdm, flash_fd, EPOLLIN, &fdm_flash, term) ||
!fdm_add(fdm, blink_fd, EPOLLIN, &fdm_blink, term) ||
!fdm_add(fdm, cursor_blink_fd, EPOLLIN, &fdm_cursor_blink, term) ||
!fdm_add(fdm, delay_lower_fd, EPOLLIN, &fdm_delayed_render, term) ||
!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))
{
goto err;
}
/* Initialize configure-based terminal attributes */
*term = (struct terminal) {
.fdm = fdm,
.quit = false,
.ptmx = ptmx,
.ptmx_buffer = tll_init(),
.cursor_keys_mode = CURSOR_KEYS_NORMAL,
.keypad_keys_mode = KEYPAD_NUMERICAL,
.auto_margin = true,
.window_title_stack = tll_init(),
.scale = 1,
.flash = {.fd = flash_fd},
.blink = {.fd = blink_fd},
.vt = {
.state = 1, /* STATE_GROUND */
},
.colors = {
.fg = conf->colors.fg,
.bg = conf->colors.bg,
.default_fg = conf->colors.fg,
.default_bg = conf->colors.bg,
.default_table = {
conf->colors.regular[0],
conf->colors.regular[1],
conf->colors.regular[2],
conf->colors.regular[3],
conf->colors.regular[4],
conf->colors.regular[5],
conf->colors.regular[6],
conf->colors.regular[7],
conf->colors.bright[0],
conf->colors.bright[1],
conf->colors.bright[2],
conf->colors.bright[3],
conf->colors.bright[4],
conf->colors.bright[5],
conf->colors.bright[6],
conf->colors.bright[7],
},
.alpha = conf->colors.alpha,
},
.origin = ORIGIN_ABSOLUTE,
.default_cursor_style = conf->cursor.style,
.cursor_style = conf->cursor.style,
.cursor_blink = {
.active = false,
.state = CURSOR_BLINK_ON,
.fd = cursor_blink_fd,
},
.default_cursor_color = {
.text = conf->cursor.color.text,
.cursor = conf->cursor.color.cursor,
},
.cursor_color = {
.text = conf->cursor.color.text,
.cursor = conf->cursor.color.cursor,
},
.xcursor = "text",
.selection = {
.start = {-1, -1},
.end = {-1, -1},
},
.normal = {.damage = tll_init(), .scroll_damage = tll_init()},
.alt = {.damage = tll_init(), .scroll_damage = tll_init()},
.grid = &term->normal,
.tab_stops = tll_init(),
.wl = wayl,
.render = {
.scrollback_lines = conf->scrollback_lines,
.app_sync_updates.timer_fd = app_sync_updates_fd,
.workers = {
.count = conf->render_worker_count,
.queue = tll_init(),
},
.presentation_timings = conf->presentation_timings,
},
.delayed_render_timer = {
.is_armed = false,
.lower_fd = delay_lower_fd,
.upper_fd = delay_upper_fd,
},
.shutdown_cb = shutdown_cb,
.shutdown_data = shutdown_data,
.foot_exe = strdup(foot_exe),
.cwd = strdup(cwd),
};
initialize_color_cube(term);
if (!initialize_render_workers(term))
goto err;
if (!initialize_fonts(term, conf))
goto err;
/* Cell dimensions are based on the font metrics. Obviously */
term->cell_width = term->fonts[0]->space_x_advance > 0
? term->fonts[0]->space_x_advance : term->fonts[0]->max_x_advance;
term->cell_height = term->fonts[0]->height;
LOG_INFO("cell width=%d, height=%d", term->cell_width, term->cell_height);
/* Start the slave/client */
if ((term->slave = slave_spawn(term->ptmx, argc, term->cwd, argv, term_env, conf->shell)) == -1)
goto err;
/* Initialize the Wayland window backend */
if ((term->window = wayl_win_init(term)) == NULL)
goto err;
/* Let the Wayland backend know we exist */
tll_push_back(wayl->terms, term);
wayl_roundtrip(term->wl);
term_set_window_title(term, "foot");
if (term->width == 0 && term->height == 0) {
/* Try to use user-configured window dimentions */
unsigned width = conf->width;
unsigned height = conf->height;
if (width == -1) {
/* No user-configuration - use 80x24 cells */
assert(height == -1);
width = 80 * term->cell_width;
height = 24 * term->cell_height;
}
/* Don't go below a single cell */
width = max(width, term->cell_width);
height = max(height, term->cell_height);
render_resize(term, width, height);
}
return term;
err:
term_destroy(term);
return NULL;
close_fds:
fdm_del(fdm, ptmx);
fdm_del(fdm, flash_fd);
fdm_del(fdm, blink_fd);
fdm_del(fdm, cursor_blink_fd);
fdm_del(fdm, delay_lower_fd);
fdm_del(fdm, delay_upper_fd);
fdm_del(fdm, app_sync_updates_fd);
free(term);
return NULL;
}
static bool
fdm_shutdown(struct fdm *fdm, int fd, int events, void *data)
{
LOG_DBG("FDM shutdown");
struct terminal *term = data;
/* Kill the event FD */
fdm_del(term->fdm, fd);
wayl_win_destroy(term->window);
term->window = NULL;
struct wayland *wayl __attribute__((unused)) = term->wl;
/*
* Normally we'd get unmapped when we destroy the Wayland
* above.
*
* However, it appears that under certain conditions, those events
* are deferred (for example, when a screen locker is active), and
* thus we can get here without having been unmapped.
*/
if (wayl->kbd_focus == term)
wayl->kbd_focus = NULL;
if (wayl->mouse_focus == term)
wayl->mouse_focus = NULL;
assert(wayl->kbd_focus != term);
assert(wayl->mouse_focus != term);
void (*cb)(void *, int) = term->shutdown_cb;
void *cb_data = term->shutdown_data;
int exit_code = term_destroy(term);
if (cb != NULL)
cb(cb_data, exit_code);
return true;
}
bool
term_shutdown(struct terminal *term)
{
if (term->is_shutting_down)
return true;
term->is_shutting_down = true;
/*
* Close FDs then postpone self-destruction to the next poll
* iteration, by creating an event FD that we trigger immediately.
*/
term_cursor_blink_disable(term);
fdm_del(term->fdm, term->render.app_sync_updates.timer_fd);
fdm_del(term->fdm, term->delayed_render_timer.lower_fd);
fdm_del(term->fdm, term->delayed_render_timer.upper_fd);
fdm_del(term->fdm, term->cursor_blink.fd);
fdm_del(term->fdm, term->blink.fd);
fdm_del(term->fdm, term->flash.fd);
fdm_del(term->fdm, term->ptmx);
term->render.app_sync_updates.timer_fd = -1;
term->delayed_render_timer.lower_fd = -1;
term->delayed_render_timer.upper_fd = -1;
term->cursor_blink.fd = -1;
term->blink.fd = -1;
term->flash.fd = -1;
term->ptmx = -1;
int event_fd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK);
if (event_fd == -1) {
LOG_ERRNO("failed to create terminal shutdown event FD");
return false;
}
if (!fdm_add(term->fdm, event_fd, EPOLLIN, &fdm_shutdown, term)) {
close(event_fd);
return false;
}
if (write(event_fd, &(uint64_t){1}, sizeof(uint64_t)) != sizeof(uint64_t)) {
LOG_ERRNO("failed to send terminal shutdown event");
fdm_del(term->fdm, event_fd);
return false;
}
return true;
}
static volatile sig_atomic_t alarm_raised;
static void
sig_alarm(int signo)
{
LOG_DBG("SIGALRM");
alarm_raised = 1;
}
int
term_destroy(struct terminal *term)
{
if (term == NULL)
return 0;
tll_foreach(term->wl->terms, it) {
if (it->item == term) {
tll_remove(term->wl->terms, it);
break;
}
}
fdm_del(term->fdm, term->render.app_sync_updates.timer_fd);
fdm_del(term->fdm, term->delayed_render_timer.lower_fd);
fdm_del(term->fdm, term->delayed_render_timer.upper_fd);
fdm_del(term->fdm, term->cursor_blink.fd);
fdm_del(term->fdm, term->blink.fd);
fdm_del(term->fdm, term->flash.fd);
fdm_del(term->fdm, term->ptmx);
if (term->window != NULL)
wayl_win_destroy(term->window);
mtx_lock(&term->render.workers.lock);
assert(tll_length(term->render.workers.queue) == 0);
/* Count livinig threads - we may get here when only some of the
* threads have been successfully started */
size_t worker_count = 0;
for (size_t i = 0; i < term->render.workers.count; i++, worker_count++) {
if (term->render.workers.threads[i] == 0)
break;
}
for (size_t i = 0; i < worker_count; i++) {
sem_post(&term->render.workers.start);
tll_push_back(term->render.workers.queue, -2);
}
cnd_broadcast(&term->render.workers.cond);
mtx_unlock(&term->render.workers.lock);
free(term->vt.osc.data);
for (int row = 0; row < term->normal.num_rows; row++)
grid_row_free(term->normal.rows[row]);
free(term->normal.rows);
for (int row = 0; row < term->alt.num_rows; row++)
grid_row_free(term->alt.rows[row]);
free(term->alt.rows);
free(term->window_title);
tll_free_and_free(term->window_title_stack, free);
for (size_t i = 0; i < sizeof(term->fonts) / sizeof(term->fonts[0]); i++)
font_destroy(term->fonts[i]);
free(term->search.buf);
for (size_t i = 0; i < term->render.workers.count; i++) {
if (term->render.workers.threads[i] != 0)
thrd_join(term->render.workers.threads[i], NULL);
}
free(term->render.workers.threads);
cnd_destroy(&term->render.workers.cond);
mtx_destroy(&term->render.workers.lock);
sem_destroy(&term->render.workers.start);
sem_destroy(&term->render.workers.done);
assert(tll_length(term->render.workers.queue) == 0);
tll_free(term->render.workers.queue);
tll_foreach(term->ptmx_buffer, it)
free(it->item.data);
tll_free(term->ptmx_buffer);
tll_free(term->tab_stops);
free(term->foot_exe);
free(term->cwd);
int ret = EXIT_SUCCESS;
if (term->slave > 0) {
LOG_DBG("waiting for slave (PID=%u) to die", term->slave);
/*
* Note: we've closed ptmx, so the slave *should* exit...
*
* But, since it is possible to write clients that ignore
* this, we need to handle it in *some* way.
*
* So, what we do is register a SIGALRM handler, and configure
* a 2 second alarm. If the slave hasn't died after this time,
* we send it a SIGTERM, then wait another 2 seconds (using
* the same alarm mechanism). If it still hasn't died, we send
* it a SIGKILL.
*
* Note that this solution is *not* asynchronous, and any
* other events etc will be ignored during this time. This of
* course only applies to a 'foot --server' instance, where
* there might be other terminals running.
*/
sigaction(SIGALRM, &(const struct sigaction){.sa_handler = &sig_alarm}, NULL);
alarm(2);
int status;
int kill_signal = SIGTERM;
while (true) {
int r = waitpid(term->slave, &status, 0);
if (r == term->slave)
break;
if (r == -1) {
assert(errno == EINTR);
if (alarm_raised) {
LOG_DBG("slave hasn't died yet, sending: %s (%d)",
kill_signal == SIGTERM ? "SIGTERM" : "SIGKILL",
kill_signal);
kill(term->slave, kill_signal);
alarm_raised = 0;
if (kill_signal != SIGKILL)
alarm(2);
kill_signal = SIGKILL;
}
}
}
/* Cancel alarm */
alarm(0);
sigaction(SIGALRM, &(const struct sigaction){.sa_handler = SIG_DFL}, NULL);
ret = EXIT_FAILURE;
if (WIFEXITED(status)) {
ret = WEXITSTATUS(status);
LOG_DBG("slave exited with code %d", ret);
} else if (WIFSIGNALED(status)) {
ret = WTERMSIG(status);
LOG_WARN("slave exited with signal %d (%s)", ret, strsignal(ret));
} else {
LOG_WARN("slave exited for unknown reason (status = 0x%08x)", status);
}
}
free(term);
#if defined(__GLIBC__)
if (!malloc_trim(0))
LOG_WARN("failed to trim memory");
#endif
return ret;
}
void
term_reset(struct terminal *term, bool hard)
{
term->cursor_keys_mode = CURSOR_KEYS_NORMAL;
term->keypad_keys_mode = KEYPAD_NUMERICAL;
term->reverse = false;
term->hide_cursor = false;
term->auto_margin = true;
term->insert_mode = false;
term->bracketed_paste = false;
term->focus_events = false;
term->mouse_tracking = MOUSE_NONE;
term->mouse_reporting = MOUSE_NORMAL;
term->charsets.selected = 0;
term->charsets.set[0] = CHARSET_ASCII;
term->charsets.set[1] = CHARSET_ASCII;
term->charsets.set[2] = CHARSET_ASCII;
term->charsets.set[3] = CHARSET_ASCII;
term->saved_charsets = term->charsets;
tll_free_and_free(term->window_title_stack, free);
free(term->window_title);
term->window_title = strdup("foot");
term->scroll_region.start = 0;
term->scroll_region.end = term->rows;
free(term->vt.osc.data);
memset(&term->vt, 0, sizeof(term->vt));
term->vt.state = 1; /* GROUND */
if (term->grid == &term->alt) {
term->grid = &term->normal;
term_restore_cursor(term);
selection_cancel(term);
}
if (!hard)
return;
term->flash.active = false;
term->blink.active = false;
term->blink.state = BLINK_ON;
term->colors.fg = term->colors.default_fg;
term->colors.bg = term->colors.default_bg;
for (size_t i = 0; i < 256; i++)
term->colors.table[i] = term->colors.default_table[i];
term->origin = ORIGIN_ABSOLUTE;
term->cursor.lcf = false;
term->cursor = (struct cursor){.point = {0, 0}};
term->saved_cursor = (struct cursor){.point = {0, 0}};
term->alt_saved_cursor = (struct cursor){.point = {0, 0}};
term->cursor_style = term->default_cursor_style;
term_cursor_blink_disable(term);
term->cursor_color.text = term->default_cursor_color.text;
term->cursor_color.cursor = term->default_cursor_color.cursor;
selection_cancel(term);
term->normal.offset = term->normal.view = 0;
term->alt.offset = term->alt.view = 0;
for (size_t i = 0; i < term->rows; i++) {
memset(grid_row_and_alloc(&term->normal, i)->cells, 0, term->cols * sizeof(struct cell));
memset(grid_row_and_alloc(&term->alt, i)->cells, 0, term->cols * sizeof(struct cell));
}
for (size_t i = term->rows; i < term->normal.num_rows; i++) {
grid_row_free(term->normal.rows[i]);
term->normal.rows[i] = NULL;
}
for (size_t i = term->rows; i < term->alt.num_rows; i++) {
grid_row_free(term->alt.rows[i]);
term->alt.rows[i] = NULL;
}
term->normal.cur_row = term->normal.rows[0];
term->alt.cur_row = term->alt.rows[0];
tll_free(term->normal.damage);
tll_free(term->normal.scroll_damage);
tll_free(term->alt.damage);
tll_free(term->alt.scroll_damage);
term->render.last_cursor.cell = NULL;
term->render.was_flashing = false;
term_damage_all(term);
}
void
term_damage_rows(struct terminal *term, int start, int end)
{
assert(start <= end);
for (int r = start; r <= end; r++) {
struct row *row = grid_row(term->grid, r);
row->dirty = true;
for (int c = 0; c < term->grid->num_cols; c++)
row->cells[c].attrs.clean = 0;
}
}
void
term_damage_rows_in_view(struct terminal *term, int start, int end)
{
assert(start <= end);
for (int r = start; r <= end; r++) {
struct row *row = grid_row_in_view(term->grid, r);
row->dirty = true;
for (int c = 0; c < term->grid->num_cols; c++)
row->cells[c].attrs.clean = 0;
}
}
void
term_damage_all(struct terminal *term)
{
term_damage_rows(term, 0, term->rows - 1);
}
void
term_damage_view(struct terminal *term)
{
term_damage_rows_in_view(term, 0, term->rows - 1);
}
void
term_damage_scroll(struct terminal *term, enum damage_type damage_type,
struct scroll_region region, int lines)
{
if (tll_length(term->grid->scroll_damage) > 0) {
struct damage *dmg = &tll_back(term->grid->scroll_damage);
if (dmg->type == damage_type &&
dmg->scroll.region.start == region.start &&
dmg->scroll.region.end == region.end)
{
dmg->scroll.lines += lines;
return;
}
}
struct damage dmg = {
.type = damage_type,
.scroll = {.region = region, .lines = lines},
};
tll_push_back(term->grid->scroll_damage, dmg);
}
static inline void
erase_cell_range(struct terminal *term, struct row *row, int start, int end)
{
assert(start < term->cols);
assert(end < term->cols);
if (unlikely(term->vt.attrs.have_bg)) {
for (int col = start; col <= end; col++) {
struct cell *c = &row->cells[col];
c->wc = 0;
c->attrs = (struct attributes){.have_bg = 1, .bg = term->vt.attrs.bg};
}
} else
memset(&row->cells[start], 0, (end - start + 1) * sizeof(row->cells[0]));
row->dirty = true;
}
static inline void
erase_line(struct terminal *term, struct row *row)
{
erase_cell_range(term, row, 0, term->cols - 1);
}
void
term_erase(struct terminal *term, const struct coord *start, const struct coord *end)
{
assert(start->row <= end->row);
assert(start->col <= end->col || start->row < end->row);
if (start->row == end->row) {
struct row *row = grid_row(term->grid, start->row);
erase_cell_range(term, row, start->col, end->col);
return;
}
assert(end->row > start->row);
erase_cell_range(
term, grid_row(term->grid, start->row), start->col, term->cols - 1);
for (int r = start->row + 1; r < end->row; r++)
erase_line(term, grid_row(term->grid, r));
erase_cell_range(term, grid_row(term->grid, end->row), 0, end->col);
}
int
term_row_rel_to_abs(const struct terminal *term, int row)
{
switch (term->origin) {
case ORIGIN_ABSOLUTE:
return min(row, term->rows - 1);
case ORIGIN_RELATIVE:
return min(row + term->scroll_region.start, term->scroll_region.end - 1);
}
assert(false);
return -1;
}
void
term_cursor_to(struct terminal *term, int row, int col)
{
assert(row < term->rows);
assert(col < term->cols);
term->cursor.lcf = false;
term->cursor.point.col = col;
term->cursor.point.row = row;
term->grid->cur_row = grid_row(term->grid, row);
}
void
term_cursor_home(struct terminal *term)
{
term_cursor_to(term, term_row_rel_to_abs(term, 0), 0);
}
void
term_cursor_left(struct terminal *term, int count)
{
int move_amount = min(term->cursor.point.col, count);
term->cursor.point.col -= move_amount;
assert(term->cursor.point.col >= 0);
term->cursor.lcf = false;
}
void
term_cursor_right(struct terminal *term, int count)
{
int move_amount = min(term->cols - term->cursor.point.col - 1, count);
term->cursor.point.col += move_amount;
assert(term->cursor.point.col < term->cols);
term->cursor.lcf = false;
}
void
term_cursor_up(struct terminal *term, int count)
{
int top = term->origin == ORIGIN_ABSOLUTE ? 0 : term->scroll_region.start;
assert(term->cursor.point.row >= top);
int move_amount = min(term->cursor.point.row - top, count);
term_cursor_to(term, term->cursor.point.row - move_amount, term->cursor.point.col);
}
void
term_cursor_down(struct terminal *term, int count)
{
int bottom = term->origin == ORIGIN_ABSOLUTE ? term->rows : term->scroll_region.end;
assert(bottom >= term->cursor.point.row);
int move_amount = min(bottom - term->cursor.point.row - 1, count);
term_cursor_to(term, term->cursor.point.row + move_amount, term->cursor.point.col);
}
static bool
cursor_blink_start_timer(struct terminal *term)
{
static const struct itimerspec timer = {
.it_value = {.tv_sec = 0, .tv_nsec = 500000000},
.it_interval = {.tv_sec = 0, .tv_nsec = 500000000},
};
if (timerfd_settime(term->cursor_blink.fd, 0, &timer, NULL) < 0) {
LOG_ERRNO("failed to arm cursor blink timer");
return false;
}
return true;
}
static bool
cursor_blink_stop_timer(struct terminal *term)
{
return timerfd_settime(term->cursor_blink.fd, 0, &(struct itimerspec){{0}}, NULL) == 0;
}
void
term_cursor_blink_enable(struct terminal *term)
{
term->cursor_blink.state = CURSOR_BLINK_ON;
term->cursor_blink.active = term->wl->kbd_focus == term
? cursor_blink_start_timer(term) : true;
}
void
term_cursor_blink_disable(struct terminal *term)
{
term->cursor_blink.active = false;
term->cursor_blink.state = CURSOR_BLINK_ON;
cursor_blink_stop_timer(term);
}
void
term_cursor_blink_restart(struct terminal *term)
{
if (term->cursor_blink.active) {
term->cursor_blink.state = CURSOR_BLINK_ON;
term->cursor_blink.active = term->wl->kbd_focus == term
? cursor_blink_start_timer(term) : true;
}
}
void
term_scroll_partial(struct terminal *term, struct scroll_region region, int rows)
{
LOG_DBG("scroll: rows=%d, region.start=%d, region.end=%d",
rows, region.start, region.end);
#if 0
if (rows > region.end - region.start) {
/* For now, clamp */
rows = region.end - region.start;
}
#endif
bool view_follows = term->grid->view == term->grid->offset;
term->grid->offset += rows;
term->grid->offset &= term->grid->num_rows - 1;
if (view_follows)
term->grid->view = term->grid->offset;
/* Top non-scrolling region. */
for (int i = region.start - 1; i >= 0; i--)
grid_swap_row(term->grid, i - rows, i, false);
/* Bottom non-scrolling region */
for (int i = term->rows - 1; i >= region.end; i--)
grid_swap_row(term->grid, i - rows, i, false);
/* Erase scrolled in lines */
for (int r = max(region.end - rows, region.start); r < region.end; r++) {
erase_line(term, grid_row_and_alloc(term->grid, r));
if (selection_on_row_in_view(term, r))
selection_cancel(term);
}
term_damage_scroll(term, DAMAGE_SCROLL, region, rows);
term->grid->cur_row = grid_row(term->grid, term->cursor.point.row);
}
void
term_scroll(struct terminal *term, int rows)
{
term_scroll_partial(term, term->scroll_region, rows);
}
void
term_scroll_reverse_partial(struct terminal *term,
struct scroll_region region, int rows)
{
LOG_DBG("scroll reverse: rows=%d, region.start=%d, region.end=%d",
rows, region.start, region.end);
#if 0
if (rows > region.end - region.start) {
/* For now, clamp */
rows = region.end - region.start;
}
#endif
bool view_follows = term->grid->view == term->grid->offset;
term->grid->offset -= rows;
while (term->grid->offset < 0)
term->grid->offset += term->grid->num_rows;
term->grid->offset &= term->grid->num_rows - 1;
assert(term->grid->offset >= 0);
assert(term->grid->offset < term->grid->num_rows);
if (view_follows)
term->grid->view = term->grid->offset;
/* Bottom non-scrolling region */
for (int i = region.end + rows; i < term->rows + rows; i++)
grid_swap_row(term->grid, i, i - rows, false);
/* Top non-scrolling region */
for (int i = 0 + rows; i < region.start + rows; i++)
grid_swap_row(term->grid, i, i - rows, false);
/* Erase scrolled in lines */
for (int r = region.start; r < min(region.start + rows, region.end); r++) {
erase_line(term, grid_row_and_alloc(term->grid, r));
if (selection_on_row_in_view(term, r))
selection_cancel(term);
}
term_damage_scroll(term, DAMAGE_SCROLL_REVERSE, region, rows);
term->grid->cur_row = grid_row(term->grid, term->cursor.point.row);
}
void
term_scroll_reverse(struct terminal *term, int rows)
{
term_scroll_reverse_partial(term, term->scroll_region, rows);
}
void
term_linefeed(struct terminal *term)
{
if (term->cursor.point.row == term->scroll_region.end - 1)
term_scroll(term, 1);
else
term_cursor_down(term, 1);
}
void
term_reverse_index(struct terminal *term)
{
if (term->cursor.point.row == term->scroll_region.start)
term_scroll_reverse(term, 1);
else
term_cursor_up(term, 1);
}
void
term_reset_view(struct terminal *term)
{
if (term->grid->view == term->grid->offset)
return;
term->grid->view = term->grid->offset;
term_damage_view(term);
}
void
term_restore_cursor(struct terminal *term)
{
int row = min(term->saved_cursor.point.row, term->rows - 1);
int col = min(term->saved_cursor.point.col, term->cols - 1);
term_cursor_to(term, row, col);
term->cursor.lcf = term->saved_cursor.lcf;
}
void
term_visual_focus_in(struct terminal *term)
{
if (term->visual_focus)
return;
term->visual_focus = true;
if (term->cursor_blink.active)
cursor_blink_start_timer(term);
cursor_refresh(term);
}
void
term_visual_focus_out(struct terminal *term)
{
if (!term->visual_focus)
return;
term->visual_focus = false;
if (term->cursor_blink.active)
cursor_blink_stop_timer(term);
cursor_refresh(term);
}
void
term_kbd_focus_in(struct terminal *term)
{
if (term->focus_events)
term_to_slave(term, "\033[I", 3);
}
void
term_kbd_focus_out(struct terminal *term)
{
if (term->focus_events)
term_to_slave(term, "\033[O", 3);
}
static int
linux_mouse_button_to_x(int button)
{
switch (button) {
case BTN_LEFT: return 1;
case BTN_MIDDLE: return 2;
case BTN_RIGHT: return 3;
case BTN_BACK: return 4;
case BTN_FORWARD: return 5;
case BTN_SIDE: return 8;
case BTN_EXTRA: return 9;
case BTN_TASK: return -1; /* TODO: ??? */
default:
LOG_WARN("unrecognized mouse button: %d (0x%x)", button, button);
return -1;
}
}
static int
encode_xbutton(int xbutton)
{
switch (xbutton) {
case 1: case 2: case 3:
return xbutton - 1;
case 4: case 5:
/* Like button 1 and 2, but with 64 added */
return xbutton - 4 + 64;
case 6: case 7:
/* Same as 4 and 5. Note: the offset should be something else? */
return xbutton - 6 + 64;
case 8: case 9: case 10: case 11:
/* Similar to 4 and 5, but adding 128 instead of 64 */
return xbutton - 8 + 128;
default:
LOG_ERR("cannot encode X mouse button: %d", xbutton);
return -1;
}
}
static void
report_mouse_click(struct terminal *term, int encoded_button, int row, int col,
bool release)
{
char response[128];
switch (term->mouse_reporting) {
case MOUSE_NORMAL: {
int encoded_col = 32 + col + 1;
int encoded_row = 32 + row + 1;
if (encoded_col > 255 || encoded_row > 255)
return;
snprintf(response, sizeof(response), "\033[M%c%c%c",
32 + (release ? 3 : encoded_button), encoded_col, encoded_row);
break;
}
case MOUSE_SGR:
snprintf(response, sizeof(response), "\033[<%d;%d;%d%c",
encoded_button, col + 1, row + 1, release ? 'm' : 'M');
break;
case MOUSE_URXVT:
snprintf(response, sizeof(response), "\033[%d;%d;%dM",
32 + (release ? 3 : encoded_button), col + 1, row + 1);
break;
case MOUSE_UTF8:
/* Unimplemented */
return;
}
term_to_slave(term, response, strlen(response));
}
static void
report_mouse_motion(struct terminal *term, int encoded_button, int row, int col)
{
report_mouse_click(term, encoded_button, row, col, false);
}
bool
term_mouse_grabbed(const struct terminal *term)
{
/*
* Mouse is grabbed by us, regardless of whether mouse tracking has been enabled or not.
*/
return
term->wl->kbd_focus == term &&
term->wl->kbd.shift &&
!term->wl->kbd.alt && /*!term->wl->kbd.ctrl &&*/ !term->wl->kbd.meta;
}
void
term_mouse_down(struct terminal *term, int button, int row, int col)
{
if (term_mouse_grabbed(term))
return;
/* Map libevent button event code to X button number */
int xbutton = linux_mouse_button_to_x(button);
if (xbutton == -1)
return;
int encoded = encode_xbutton(xbutton);
if (encoded == -1)
return;
bool has_focus = term->wl->kbd_focus == term;
bool shift = has_focus ? term->wl->kbd.shift : false;
bool alt = has_focus ? term->wl->kbd.alt : false;
bool ctrl = has_focus ? term->wl->kbd.ctrl : false;
encoded += (shift ? 4 : 0) + (alt ? 8 : 0) + (ctrl ? 16 : 0);
switch (term->mouse_tracking) {
case MOUSE_NONE:
break;
case MOUSE_CLICK:
case MOUSE_DRAG:
case MOUSE_MOTION:
report_mouse_click(term, encoded, row, col, false);
break;
case MOUSE_X10:
/* Never enabled */
assert(false && "unimplemented");
break;
}
}
void
term_mouse_up(struct terminal *term, int button, int row, int col)
{
if (term_mouse_grabbed(term))
return;
/* Map libevent button event code to X button number */
int xbutton = linux_mouse_button_to_x(button);
if (xbutton == -1)
return;
if (xbutton == 4 || xbutton == 5) {
/* No release events for scroll buttons */
return;
}
int encoded = encode_xbutton(xbutton);
if (encoded == -1)
return;
bool has_focus = term->wl->kbd_focus == term;
bool shift = has_focus ? term->wl->kbd.shift : false;
bool alt = has_focus ? term->wl->kbd.alt : false;
bool ctrl = has_focus ? term->wl->kbd.ctrl : false;
encoded += (shift ? 4 : 0) + (alt ? 8 : 0) + (ctrl ? 16 : 0);
switch (term->mouse_tracking) {
case MOUSE_NONE:
break;
case MOUSE_CLICK:
case MOUSE_DRAG:
case MOUSE_MOTION:
report_mouse_click(term, encoded, row, col, true);
break;
case MOUSE_X10:
/* Never enabled */
assert(false && "unimplemented");
break;
}
}
void
term_mouse_motion(struct terminal *term, int button, int row, int col)
{
if (term_mouse_grabbed(term))
return;
int encoded = 0;
if (button != 0) {
/* Map libevent button event code to X button number */
int xbutton = linux_mouse_button_to_x(button);
if (xbutton == -1)
return;
encoded = encode_xbutton(xbutton);
if (encoded == -1)
return;
} else
encoded = 3; /* "released" */
bool has_focus = term->wl->kbd_focus == term;
bool shift = has_focus ? term->wl->kbd.shift : false;
bool alt = has_focus ? term->wl->kbd.alt : false;
bool ctrl = has_focus ? term->wl->kbd.ctrl : false;
encoded += 32; /* Motion event */
encoded += (shift ? 4 : 0) + (alt ? 8 : 0) + (ctrl ? 16 : 0);
switch (term->mouse_tracking) {
case MOUSE_NONE:
case MOUSE_CLICK:
return;
case MOUSE_DRAG:
if (button == 0)
return;
/* FALLTHROUGH */
case MOUSE_MOTION:
report_mouse_motion(term, encoded, row, col);
break;
case MOUSE_X10:
/* Never enabled */
assert(false && "unimplemented");
break;
}
}
void
term_xcursor_update(struct terminal *term)
{
term->xcursor =
term->is_searching ? XCURSOR_LEFT_PTR :
selection_enabled(term) ? XCURSOR_TEXT :
XCURSOR_HAND2;
render_xcursor_set(term);
}
void
term_set_window_title(struct terminal *term, const char *title)
{
free(term->window_title);
term->window_title = strdup(title);
render_set_title(term, term->window_title);
}
void
term_flash(struct terminal *term, unsigned duration_ms)
{
LOG_DBG("FLASH for %ums", duration_ms);
struct itimerspec alarm = {
.it_value = {.tv_sec = 0, .tv_nsec = duration_ms * 1000000},
};
if (timerfd_settime(term->flash.fd, 0, &alarm, NULL) < 0)
LOG_ERRNO("failed to arm flash timer");
else {
term->flash.active = true;
}
}
bool
term_spawn_new(const struct terminal *term)
{
pid_t pid = fork();
if (pid < 0) {
LOG_ERRNO("failed to fork new terminal");
return false;
}
if (pid == 0) {
/* Child */
int pipe_fds[2] = {-1, -1};
if (pipe2(pipe_fds, O_CLOEXEC) < 0) {
LOG_ERRNO("failed to create pipe");
goto err;
}
/* Double fork */
pid_t pid2 = fork();
if (pid2 < 0) {
LOG_ERRNO("failed to double fork new terminal");
goto err;
}
if (pid2 == 0) {
/* Child */
close(pipe_fds[0]);
chdir(term->cwd);
execlp(term->foot_exe, term->foot_exe, NULL);
write(pipe_fds[1], &errno, sizeof(errno));
_exit(errno);
}
/* Parent */
close(pipe_fds[1]);
int _errno;
static_assert(sizeof(_errno) == sizeof(errno), "errno size mismatch");
ssize_t ret = read(pipe_fds[0], &_errno, sizeof(_errno));
close(pipe_fds[0]);
if (ret == 0)
_exit(0);
else if (ret < 0)
LOG_ERRNO("failed to read from pipe");
else {
LOG_ERRNO_P("%s: failed to spawn new terminal", _errno, term->foot_exe);
errno = _errno;
waitpid(pid2, NULL, 0);
}
err:
if (pipe_fds[0] != -1)
close(pipe_fds[0]);
_exit(errno);
}
int result;
waitpid(pid, &result, 0);
return WIFEXITED(result) && WEXITSTATUS(result) == 0;
}
void
term_enable_app_sync_updates(struct terminal *term)
{
term->render.app_sync_updates.enabled = true;
if (timerfd_settime(
term->render.app_sync_updates.timer_fd, 0,
&(struct itimerspec){.it_value = {.tv_sec = 1}}, NULL) < 0)
{
LOG_ERR("failed to arm timer for application synchronized updates");
}
/* Disarm delayed rendering timers */
timerfd_settime(
term->delayed_render_timer.lower_fd, 0,
&(struct itimerspec){{0}}, NULL);
timerfd_settime(
term->delayed_render_timer.upper_fd, 0,
&(struct itimerspec){{0}}, NULL);
}
void
term_disable_app_sync_updates(struct terminal *term)
{
if (!term->render.app_sync_updates.enabled)
return;
term->render.app_sync_updates.enabled = false;
/* Reset timers */
timerfd_settime(
term->render.app_sync_updates.timer_fd, 0,
&(struct itimerspec){{0}}, NULL);
}