#include "terminal.h" #if defined(__GLIBC__) #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define LOG_MODULE "terminal" #define LOG_ENABLE_DBG 0 #include "log.h" #include "async.h" #include "commands.h" #include "config.h" #include "debug.h" #include "extract.h" #include "grid.h" #include "ime.h" #include "input.h" #include "notify.h" #include "quirks.h" #include "reaper.h" #include "render.h" #include "selection.h" #include "shm.h" #include "sixel.h" #include "slave.h" #include "spawn.h" #include "url-mode.h" #include "util.h" #include "vt.h" #include "xmalloc.h" #include "xsnprintf.h" #define PTMX_TIMING 0 static void enqueue_data_for_slave(const void *data, size_t len, size_t offset, ptmx_buffer_list_t *buffer_list) { struct ptmx_buffer queued = { .data = xmemdup(data, len), .len = len, .idx = offset, }; tll_push_back(*buffer_list, queued); } static bool data_to_slave(struct terminal *term, const void *data, size_t len, ptmx_buffer_list_t *buffer_list) { /* * Try a synchronous write first. If we fail to write everything, * switch to asynchronous. */ size_t async_idx = 0; 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; enqueue_data_for_slave(data, len, async_idx, buffer_list); return true; case ASYNC_WRITE_DONE: return true; case ASYNC_WRITE_ERR: LOG_ERRNO("failed to synchronously write %zu bytes to slave", len); return false; } BUG("Unexpected async_write() return value"); return false; } bool term_paste_data_to_slave(struct terminal *term, const void *data, size_t len) { xassert(term->is_sending_paste_data); if (term->ptmx < 0) { /* We're probably in "hold" */ return false; } if (tll_length(term->ptmx_paste_buffers) > 0) { /* Don't even try to send data *now* if there's queued up * data, since that would result in events arriving out of * order. */ enqueue_data_for_slave(data, len, 0, &term->ptmx_paste_buffers); return true; } return data_to_slave(term, data, len, &term->ptmx_paste_buffers); } bool term_to_slave(struct terminal *term, const void *data, size_t len) { if (term->ptmx < 0) { /* We're probably in "hold" */ return false; } if (tll_length(term->ptmx_buffers) > 0 || term->is_sending_paste_data) { /* * Don't even try to send data *now* if there's queued up * data, since that would result in events arriving out of * order. * * Furthermore, if we're currently sending paste data to the * client, do *not* mix that stream with other events * (https://codeberg.org/dnkl/foot/issues/101). */ enqueue_data_for_slave(data, len, 0, &term->ptmx_buffers); return true; } return data_to_slave(term, data, len, &term->ptmx_buffers); } 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 */ xassert(tll_length(term->ptmx_buffers) > 0 || tll_length(term->ptmx_paste_buffers) > 0); /* Writes a single buffer, returns if not all of it could be written */ #define write_one_buffer(buffer_list) \ { \ switch (async_write(term->ptmx, it->item.data, it->item.len, &it->item.idx)) { \ case ASYNC_WRITE_DONE: \ free(it->item.data); \ tll_remove(buffer_list, 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; \ } \ } tll_foreach(term->ptmx_paste_buffers, it) write_one_buffer(term->ptmx_paste_buffers); /* If we get here, *all* paste data buffers were successfully * flushed */ if (!term->is_sending_paste_data) { tll_foreach(term->ptmx_buffers, it) write_one_buffer(term->ptmx_buffers); } /* * If we get here, *all* buffers were successfully flushed. * * Or, we're still sending paste data, in which case we do *not* * want to send the "normal" queued up data * * In both cases, we want to *disable* the FDM callback since * otherwise we'd just be called right away again, with nothing to * write. */ fdm_event_del(term->fdm, term->ptmx, EPOLLOUT); return true; } static bool add_utmp_record(const struct config *conf, struct reaper *reaper, int ptmx) { #if defined(UTMP_ADD) if (ptmx < 0) return true; if (conf->utmp_helper_path == NULL) return true; char *const argv[] = {conf->utmp_helper_path, UTMP_ADD, getenv("WAYLAND_DISPLAY"), NULL}; return spawn(reaper, NULL, argv, ptmx, ptmx, -1, NULL, NULL, NULL) >= 0; #else return true; #endif } static bool del_utmp_record(const struct config *conf, struct reaper *reaper, int ptmx) { #if defined(UTMP_DEL) if (ptmx < 0) return true; if (conf->utmp_helper_path == NULL) return true; char *del_argument = #if defined(UTMP_DEL_HAVE_ARGUMENT) getenv("WAYLAND_DISPLAY") #else NULL #endif ; char *const argv[] = {conf->utmp_helper_path, UTMP_DEL, del_argument, NULL}; return spawn(reaper, NULL, argv, ptmx, ptmx, -1, NULL, NULL, NULL) >= 0; #else return true; #endif } #if PTMX_TIMING static struct timespec last = {0}; #endif static bool cursor_blink_rearm_timer(struct terminal *term); /* Externally visible, but not declared in terminal.h, to enable pgo * to call this function directly */ bool fdm_ptmx(struct fdm *fdm, int fd, int events, void *data) { struct terminal *term = data; const bool pollin = events & EPOLLIN; const bool pollout = events & EPOLLOUT; const bool hup = events & EPOLLHUP; if (pollout) { if (!fdm_ptmx_out(fdm, fd, events, data)) return false; } /* Prevent blinking while typing */ if (term->cursor_blink.fd >= 0) { term->cursor_blink.state = CURSOR_BLINK_ON; cursor_blink_rearm_timer(term); } if (unlikely(term->interactive_resizing.grid != NULL)) { /* * Don't consume PTMX while we're doing an interactive resize, * since the 'normal' grid we're currently using is a * temporary one - all changes done to it will be lost when * the interactive resize ends. */ return true; } uint8_t buf[24 * 1024]; const size_t max_iterations = !hup ? 10 : SIZE_MAX; for (size_t i = 0; i < max_iterations && pollin; i++) { xassert(pollin); ssize_t count = read(term->ptmx, buf, sizeof(buf)); if (count < 0) { if (errno == EAGAIN || errno == EIO) { /* * EAGAIN: no more to read - FDM will trigger us again * EIO: assume PTY was closed - we already have, or will get, a EPOLLHUP */ break; } LOG_ERRNO("failed to read from pseudo terminal"); return false; } else if (count == 0) { /* Reached end-of-file */ break; } xassert(term->interactive_resizing.grid == NULL); vt_from_slave(term, buf, count); } if (!term->render.app_sync_updates.enabled) { /* * We likely need to re-render. But, we don't want to do it * immediately. Often, a single client update is done through * multiple writes. This could lead to us rendering one frame with * "intermediate" state. * * 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 "flickering". * * 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. */ uint64_t lower_ns = term->conf->tweak.delayed_render_lower_ns; uint64_t upper_ns = term->conf->tweak.delayed_render_upper_ns; if (lower_ns > 0 && upper_ns > 0) { #if PTMX_TIMING struct timespec now; clock_gettime(CLOCK_MONOTONIC, &now); if (last.tv_sec > 0 || last.tv_nsec > 0) { struct timespec diff; timespec_sub(&now, &last, &diff); LOG_INFO("waited %lds %ldns for more input", (long)diff.tv_sec, diff.tv_nsec); } last = now; #endif xassert(lower_ns < 1000000000); xassert(upper_ns < 1000000000); xassert(upper_ns > lower_ns); timerfd_settime( term->delayed_render_timer.lower_fd, 0, &(struct itimerspec){.it_value = {.tv_nsec = lower_ns}}, 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 = upper_ns}}, NULL); term->delayed_render_timer.is_armed = true; } } else render_refresh(term); } if (hup) { del_utmp_record(term->conf, term->reaper, term->ptmx); fdm_del(fdm, fd); term->ptmx = -1; /* * Normally, we do *not* want to shutdown when the PTY is * closed. Instead, we want to wait for the client application * to exit. * * However, when we're using a pre-existing PTY (the --pty * option), there _is_ no client application. That is, foot * does *not* fork+exec anything, and thus the only way to * shutdown is to wait for the PTY to be closed. */ if (term->slave < 0 && !term->conf->hold_at_exit) { term_shutdown(term); } } return true; } bool term_ptmx_pause(struct terminal *term) { return fdm_event_del(term->fdm, term->ptmx, EPOLLIN); } bool term_ptmx_resume(struct terminal *term) { return fdm_event_add(term->fdm, term->ptmx, EPOLLIN); } 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; 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.state = BLINK_ON; fdm_del(term->fdm, term->blink.fd); term->blink.fd = -1; } else render_refresh(term); return true; } void term_arm_blink_timer(struct terminal *term) { if (term->blink.fd >= 0) return; LOG_DBG("arming blink timer"); int fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK); if (fd < 0) { LOG_ERRNO("failed to create blink timer FD"); return; } if (!fdm_add(term->fdm, fd, EPOLLIN, &fdm_blink, term)) { close(fd); return; } struct itimerspec alarm = { .it_value = {.tv_sec = 0, .tv_nsec = 500 * 1000000}, .it_interval = {.tv_sec = 0, .tv_nsec = 500 * 1000000}, }; if (timerfd_settime(fd, 0, &alarm, NULL) < 0) { LOG_ERRNO("failed to arm blink timer"); fdm_del(term->fdm, fd); } term->blink.fd = fd; } static void cursor_refresh(struct terminal *term) { term->grid->cur_row->cells[term->grid->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; 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 (ret1 == 0 && ret2 == 0) return true; #if PTMX_TIMING last = (struct timespec){0}; #endif /* 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; render_refresh(term); 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 bool fdm_title_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.title.timer_fd, &unused, sizeof(unused)); if (ret < 0) { if (errno == EAGAIN) return true; LOG_ERRNO("failed to read title update throttle timer"); return false; } struct itimerspec reset = {{0}}; timerfd_settime(term->render.title.timer_fd, 0, &reset, NULL); render_refresh_title(term); return true; } static bool fdm_app_id_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.app_id.timer_fd, &unused, sizeof(unused)); if (ret < 0) { if (errno == EAGAIN) return true; LOG_ERRNO("failed to read app ID update throttle timer"); return false; } struct itimerspec reset = {{0}}; timerfd_settime(term->render.app_id.timer_fd, 0, &reset, NULL); render_refresh_app_id(term); return true; } static bool initialize_render_workers(struct terminal *term) { LOG_INFO("using %hu rendering threads", term->render.workers.count); if (sem_init(&term->render.workers.start, 0, 0) < 0 || sem_init(&term->render.workers.done, 0, 0) < 0) { LOG_ERRNO("failed to instantiate render worker semaphores"); return false; } int err; if ((err = mtx_init(&term->render.workers.lock, mtx_plain)) != thrd_success) { LOG_ERR("failed to instantiate render worker mutex: %s (%d)", thrd_err_as_string(err), err); goto err_sem_destroy; } term->render.workers.threads = xcalloc( 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 = xmalloc(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 != thrd_success) { LOG_ERR("failed to create render worker thread: %s (%d)", thrd_err_as_string(ret), ret); term->render.workers.threads[i] = 0; return false; } } return true; err_sem_destroy: sem_destroy(&term->render.workers.start); sem_destroy(&term->render.workers.done); return false; } static void free_custom_glyph(struct fcft_glyph **glyph) { if (*glyph == NULL) return; free(pixman_image_get_data((*glyph)->pix)); pixman_image_unref((*glyph)->pix); free(*glyph); *glyph = NULL; } static void free_custom_glyphs(struct fcft_glyph ***glyphs, size_t count) { if (*glyphs == NULL) return; for (size_t i = 0; i < count; i++) free_custom_glyph(&(*glyphs)[i]); free(*glyphs); *glyphs = NULL; } static void term_line_height_update(struct terminal *term) { const struct config *conf = term->conf; if (term->conf->line_height.px < 0) { term->font_line_height.pt = 0; term->font_line_height.px = -1; return; } const float dpi = term->font_is_sized_by_dpi ? term->font_dpi : 96.; const float font_original_pt_size = conf->fonts[0].arr[0].px_size > 0 ? conf->fonts[0].arr[0].px_size * 72. / dpi : conf->fonts[0].arr[0].pt_size; const float font_current_pt_size = term->font_sizes[0][0].px_size > 0 ? term->font_sizes[0][0].px_size * 72. / dpi : term->font_sizes[0][0].pt_size; const float change = font_current_pt_size / font_original_pt_size; const float line_original_pt_size = conf->line_height.px > 0 ? conf->line_height.px * 72. / dpi : conf->line_height.pt; term->font_line_height.px = 0; term->font_line_height.pt = fmaxf(line_original_pt_size * change, 0.); } static bool term_set_fonts(struct terminal *term, struct fcft_font *fonts[static 4], bool resize_grid) { for (size_t i = 0; i < 4; i++) { xassert(fonts[i] != NULL); fcft_destroy(term->fonts[i]); term->fonts[i] = fonts[i]; } free_custom_glyphs( &term->custom_glyphs.box_drawing, GLYPH_BOX_DRAWING_COUNT); free_custom_glyphs( &term->custom_glyphs.braille, GLYPH_BRAILLE_COUNT); free_custom_glyphs( &term->custom_glyphs.legacy, GLYPH_LEGACY_COUNT); const struct config *conf = term->conf; const struct fcft_glyph *M = fcft_rasterize_char_utf32( fonts[0], U'M', term->font_subpixel); int advance = M != NULL ? M->advance.x : term->fonts[0]->max_advance.x; term_line_height_update(term); term->cell_width = advance + term_pt_or_px_as_pixels(term, &conf->letter_spacing); term->cell_height = term->font_line_height.px >= 0 ? term_pt_or_px_as_pixels(term, &term->font_line_height) : max(term->fonts[0]->height, term->fonts[0]->ascent + term->fonts[0]->descent); if (term->cell_width <= 0) term->cell_width = 1; if (term->cell_height <= 0) term->cell_height = 1; term->font_x_ofs = term_pt_or_px_as_pixels(term, &conf->horizontal_letter_offset); term->font_y_ofs = term_pt_or_px_as_pixels(term, &conf->vertical_letter_offset); term->font_baseline = term_font_baseline(term); LOG_INFO("cell width=%d, height=%d", term->cell_width, term->cell_height); sixel_cell_size_changed(term); /* Optimization - some code paths (are forced to) call * render_resize() after this function */ if (resize_grid) { /* Use force, since cell-width/height may have changed */ render_resize( term, (int)roundf(term->width / term->scale), (int)roundf(term->height / term->scale), RESIZE_FORCE | RESIZE_KEEP_GRID); } return true; } static float get_font_dpi(const struct terminal *term) { /* * Use output's DPI to scale font. This is to ensure the font has * the same physical height (if measured by a ruler) regardless of * monitor. * * Conceptually, we use the physical monitor specs to calculate * the DPI, and we ignore the output's scaling factor. * * However, to deal with legacy fractional scaling, where we're * told to render at e.g. 2x, but are then downscaled by the * compositor to e.g. 1.25, we use the scaled DPI value multiplied * by the scale factor instead. * * For integral scaling factors the resulting DPI is the same as * if we had used the physical DPI. * * For legacy fractional scaling factors we'll get a DPI *larger* * than the physical DPI, that ends up being right when later * downscaled by the compositor. * * With the newer fractional-scale-v1 protocol, we use the * monitor's real DPI, since we scale everything to the correct * scaling factor (no downscaling done by the compositor). */ xassert(tll_length(term->wl->monitors) > 0); const struct wl_window *win = term->window; const struct monitor *mon = NULL; if (tll_length(win->on_outputs) > 0) mon = tll_back(win->on_outputs); else { if (term->font_dpi_before_unmap > 0.) { /* * Use last known "good" DPI * * This avoids flickering when window is unmapped/mapped * (some compositors do this when a window is minimized), * on a multi-monitor setup with different monitor DPIs. */ return term->font_dpi_before_unmap; } if (tll_length(term->wl->monitors) > 0) mon = &tll_front(term->wl->monitors); } const float monitor_dpi = mon != NULL ? term_fractional_scaling(term) ? mon->dpi.physical : mon->dpi.scaled : 96.; return monitor_dpi > 0. ? monitor_dpi : 96.; } static enum fcft_subpixel get_font_subpixel(const struct terminal *term) { if (term->colors.alpha != 0xffff) { /* Can't do subpixel rendering on transparent background */ return FCFT_SUBPIXEL_NONE; } enum wl_output_subpixel wl_subpixel; /* * Wayland doesn't tell us *which* part of the surface that goes * on a specific output, only whether the surface is mapped to an * output or not. * * Thus, when determining which subpixel mode to use, we can't do * much but select *an* output. So, we pick the one we were most * recently mapped on. * * If we're not mapped at all, we pick the first available * monitor, and hope that's where we'll eventually get mapped. * * If there aren't any monitors we use the "default" subpixel * mode. */ if (tll_length(term->window->on_outputs) > 0) wl_subpixel = tll_back(term->window->on_outputs)->subpixel; else if (tll_length(term->wl->monitors) > 0) wl_subpixel = tll_front(term->wl->monitors).subpixel; else wl_subpixel = WL_OUTPUT_SUBPIXEL_UNKNOWN; switch (wl_subpixel) { case WL_OUTPUT_SUBPIXEL_UNKNOWN: return FCFT_SUBPIXEL_DEFAULT; case WL_OUTPUT_SUBPIXEL_NONE: return FCFT_SUBPIXEL_NONE; case WL_OUTPUT_SUBPIXEL_HORIZONTAL_RGB: return FCFT_SUBPIXEL_HORIZONTAL_RGB; case WL_OUTPUT_SUBPIXEL_HORIZONTAL_BGR: return FCFT_SUBPIXEL_HORIZONTAL_BGR; case WL_OUTPUT_SUBPIXEL_VERTICAL_RGB: return FCFT_SUBPIXEL_VERTICAL_RGB; case WL_OUTPUT_SUBPIXEL_VERTICAL_BGR: return FCFT_SUBPIXEL_VERTICAL_BGR; } return FCFT_SUBPIXEL_DEFAULT; } int term_pt_or_px_as_pixels(const struct terminal *term, const struct pt_or_px *pt_or_px) { float scale = !term->font_is_sized_by_dpi ? term->scale : 1.; float dpi = term->font_is_sized_by_dpi ? term->font_dpi : 96.; return pt_or_px->px == 0 ? (int)roundf(pt_or_px->pt * scale * dpi / 72) : (int)roundf(pt_or_px->px * scale); } struct font_load_data { size_t count; const char **names; const char *attrs; struct fcft_font **font; }; static int font_loader_thread(void *_data) { struct font_load_data *data = _data; *data->font = fcft_from_name(data->count, data->names, data->attrs); return *data->font != NULL; } static bool reload_fonts(struct terminal *term, bool resize_grid) { const struct config *conf = term->conf; const size_t counts[4] = { conf->fonts[0].count, conf->fonts[1].count, conf->fonts[2].count, conf->fonts[3].count, }; /* Configure size (which may have been changed run-time) */ char **names[4]; for (size_t i = 0; i < 4; i++) { names[i] = xmalloc(counts[i] * sizeof(names[i][0])); const struct config_font_list *font_list = &conf->fonts[i]; for (size_t j = 0; j < font_list->count; j++) { const struct config_font *font = &font_list->arr[j]; bool use_px_size = term->font_sizes[i][j].px_size > 0; char size[64]; const float scale = term->font_is_sized_by_dpi ? 1. : term->scale; if (use_px_size) snprintf(size, sizeof(size), ":pixelsize=%d", (int)roundf(term->font_sizes[i][j].px_size * scale)); else snprintf(size, sizeof(size), ":size=%.2f", term->font_sizes[i][j].pt_size * scale); names[i][j] = xstrjoin(font->pattern, size); } } /* Did user configure custom bold/italic fonts? * Or should we use the regular font, with weight/slant attributes? */ const bool custom_bold = counts[1] > 0; const bool custom_italic = counts[2] > 0; const bool custom_bold_italic = counts[3] > 0; const size_t count_regular = counts[0]; const char **names_regular = (const char **)names[0]; const size_t count_bold = custom_bold ? counts[1] : counts[0]; const char **names_bold = (const char **)(custom_bold ? names[1] : names[0]); const size_t count_italic = custom_italic ? counts[2] : counts[0]; const char **names_italic = (const char **)(custom_italic ? names[2] : names[0]); const size_t count_bold_italic = custom_bold_italic ? counts[3] : counts[0]; const char **names_bold_italic = (const char **)(custom_bold_italic ? names[3] : names[0]); const bool use_dpi = term->font_is_sized_by_dpi; char *dpi = xasprintf("dpi=%.2f", use_dpi ? term->font_dpi : 96.); char *attrs[4] = { [0] = dpi, /* Takes ownership */ [1] = xstrjoin(dpi, !custom_bold ? ":weight=bold" : ""), [2] = xstrjoin(dpi, !custom_italic ? ":slant=italic" : ""), [3] = xstrjoin(dpi, !custom_bold_italic ? ":weight=bold:slant=italic" : ""), }; struct fcft_font *fonts[4]; struct font_load_data data[4] = { {count_regular, names_regular, attrs[0], &fonts[0]}, {count_bold, names_bold, attrs[1], &fonts[1]}, {count_italic, names_italic, attrs[2], &fonts[2]}, {count_bold_italic, names_bold_italic, attrs[3], &fonts[3]}, }; thrd_t tids[4] = {0}; for (size_t i = 0; i < 4; i++) { int ret = thrd_create(&tids[i], &font_loader_thread, &data[i]); if (ret != thrd_success) { LOG_ERR("failed to create font loader thread: %s (%d)", thrd_err_as_string(ret), ret); break; } } bool success = true; for (size_t i = 0; i < 4; i++) { if (tids[i] != 0) { int ret; if (thrd_join(tids[i], &ret) != 0) success = false; else success = success && ret; } else success = false; } for (size_t i = 0; i < 4; i++) { for (size_t j = 0; j < counts[i]; j++) free(names[i][j]); free(names[i]); free(attrs[i]); } if (!success) { LOG_ERR("failed to load primary fonts"); for (size_t i = 0; i < 4; i++) { fcft_destroy(fonts[i]); fonts[i] = NULL; } } return success ? term_set_fonts(term, fonts, resize_grid) : success; } static bool load_fonts_from_conf(struct terminal *term) { const struct config *conf = term->conf; for (size_t i = 0; i < 4; i++) { const struct config_font_list *font_list = &conf->fonts[i]; for (size_t j = 0; j < font_list->count; j++) { const struct config_font *font = &font_list->arr[j]; term->font_sizes[i][j] = (struct config_font){ .pt_size = font->pt_size, .px_size = font->px_size}; } } return reload_fonts(term, true); } static void fdm_client_terminated( struct reaper *reaper, pid_t pid, int status, void *data); static const int PTY_OPEN_FLAGS = O_RDWR | O_NOCTTY; struct terminal * term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, struct wayland *wayl, const char *foot_exe, const char *cwd, const char *token, const char *pty_path, int argc, char *const *argv, const char *const *envp, void (*shutdown_cb)(void *data, int exit_code), void *shutdown_data) { int ptmx = -1; int flash_fd = -1; int delay_lower_fd = -1; int delay_upper_fd = -1; int app_sync_updates_fd = -1; int title_update_fd = -1; int app_id_update_fd = -1; struct terminal *term = malloc(sizeof(*term)); if (unlikely(term == NULL)) { LOG_ERRNO("malloc() failed"); return NULL; } ptmx = pty_path ? open(pty_path, PTY_OPEN_FLAGS) : posix_openpt(PTY_OPEN_FLAGS); if (ptmx < 0) { LOG_ERRNO("failed to open PTY"); goto close_fds; } if ((flash_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0) { LOG_ERRNO("failed to create flash timer FD"); goto close_fds; } if ((delay_lower_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0 || (delay_upper_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0) { 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)) < 0) { LOG_ERRNO("failed to create application synchronized updates timer FD"); goto close_fds; } if ((title_update_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0) { LOG_ERRNO("failed to create title 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"); goto close_fds; } if (ioctl(ptmx, (unsigned int)TIOCSWINSZ, &(struct winsize){.ws_row = 24, .ws_col = 80}) < 0) { LOG_ERRNO("failed to set initial TIOCSWINSZ"); goto close_fds; } /* Need to register *very* early (before the first "goto err"), to * ensure term_destroy() doesn't unref a key-binding we haven't * yet ref:d */ key_binding_new_for_conf(wayl->key_binding_manager, wayl, conf); 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; } /* * Enable all FDM callbackes *except* ptmx - we can't do that * until the window has been 'configured' since we don't have a * size (and thus no grid) before then. */ if (!fdm_add(fdm, flash_fd, EPOLLIN, &fdm_flash, 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) || !fdm_add(fdm, title_update_fd, EPOLLIN, &fdm_title_update_timeout, term) || !fdm_add(fdm, app_id_update_fd, EPOLLIN, &fdm_app_id_update_timeout, term)) { goto err; } /* Initialize configure-based terminal attributes */ *term = (struct terminal) { .fdm = fdm, .reaper = reaper, .conf = conf, .slave = -1, .ptmx = ptmx, .ptmx_buffers = tll_init(), .ptmx_paste_buffers = tll_init(), .font_sizes = { xmalloc(sizeof(term->font_sizes[0][0]) * conf->fonts[0].count), xmalloc(sizeof(term->font_sizes[1][0]) * conf->fonts[1].count), xmalloc(sizeof(term->font_sizes[2][0]) * conf->fonts[2].count), xmalloc(sizeof(term->font_sizes[3][0]) * conf->fonts[3].count), }, .font_dpi = 0., .font_dpi_before_unmap = -1., .font_subpixel = (conf->colors.alpha == 0xffff /* Can't do subpixel rendering on transparent background */ ? FCFT_SUBPIXEL_DEFAULT : FCFT_SUBPIXEL_NONE), .cursor_keys_mode = CURSOR_KEYS_NORMAL, .keypad_keys_mode = KEYPAD_NUMERICAL, .reverse_wrap = true, .auto_margin = true, .window_title_stack = tll_init(), .scale = 1., .scale_before_unmap = -1, .flash = {.fd = flash_fd}, .blink = {.fd = -1}, .vt = { .state = 0, /* STATE_GROUND */ }, .colors = { .fg = conf->colors.fg, .bg = conf->colors.bg, .alpha = conf->colors.alpha, .cursor_fg = conf->cursor.color.text, .cursor_bg = conf->cursor.color.cursor, .selection_fg = conf->colors.selection_fg, .selection_bg = conf->colors.selection_bg, .use_custom_selection = conf->colors.use_custom.selection, }, .color_stack = { .stack = NULL, .size = 0, .idx = 0, }, .origin = ORIGIN_ABSOLUTE, .cursor_style = conf->cursor.style, .cursor_blink = { .decset = false, .deccsusr = conf->cursor.blink.enabled, .state = CURSOR_BLINK_ON, .fd = -1, }, .selection = { .coords = { .start = {-1, -1}, .end = {-1, -1}, }, .pivot = { .start = {-1, -1}, .end = {-1, -1}, }, .auto_scroll = { .fd = -1, }, }, .normal = {.scroll_damage = tll_init(), .sixel_images = tll_init()}, .alt = {.scroll_damage = tll_init(), .sixel_images = tll_init()}, .grid = &term->normal, .composed = NULL, .alt_scrolling = conf->mouse.alternate_scroll_mode, .meta = { .esc_prefix = true, .eight_bit = true, }, .num_lock_modifier = true, .bell_action_enabled = true, .tab_stops = tll_init(), .wl = wayl, .render = { .chains = { .grid = shm_chain_new(wayl->shm, true, 1 + conf->render_worker_count), .search = shm_chain_new(wayl->shm, false, 1), .scrollback_indicator = shm_chain_new(wayl->shm, false, 1), .render_timer = shm_chain_new(wayl->shm, false, 1), .url = shm_chain_new(wayl->shm, false, 1), .csd = shm_chain_new(wayl->shm, false, 1), .overlay = shm_chain_new(wayl->shm, false, 1), }, .scrollback_lines = conf->scrollback.lines, .app_sync_updates.timer_fd = app_sync_updates_fd, .title = { .timer_fd = title_update_fd, }, .app_id = { .timer_fd = app_id_update_fd, }, .workers = { .count = conf->render_worker_count, .queue = tll_init(), }, }, .delayed_render_timer = { .is_armed = false, .lower_fd = delay_lower_fd, .upper_fd = delay_upper_fd, }, .sixel = { .scrolling = true, .use_private_palette = true, .palette_size = SIXEL_MAX_COLORS, .max_width = SIXEL_MAX_WIDTH, .max_height = SIXEL_MAX_HEIGHT, }, .shutdown = { .terminate_timeout_fd = -1, .cb = shutdown_cb, .cb_data = shutdown_data, }, .foot_exe = xstrdup(foot_exe), .cwd = xstrdup(cwd), .grapheme_shaping = conf->tweak.grapheme_shaping, #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED .ime_enabled = true, #endif .active_notifications = tll_init(), }; pixman_region32_init(&term->render.last_overlay_clip); term_update_ascii_printer(term); for (size_t i = 0; i < 4; i++) { const struct config_font_list *font_list = &conf->fonts[i]; for (size_t j = 0; j < font_list->count; j++) { const struct config_font *font = &font_list->arr[j]; term->font_sizes[i][j] = (struct config_font){ .pt_size = font->pt_size, .px_size = font->px_size}; } } for (size_t i = 0; i < ALEN(term->notification_icons); i++) { term->notification_icons[i].tmp_file_fd = -1; } 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; } reaper_add(term->reaper, term->slave, &fdm_client_terminated, term); } /* Guess scale; we're not mapped yet, so we don't know on which * output we'll be. Use scaling factor from first monitor */ xassert(tll_length(term->wl->monitors) > 0); term->scale = tll_front(term->wl->monitors).scale; memcpy(term->colors.table, term->conf->colors.table, sizeof(term->colors.table)); /* Initialize the Wayland window backend */ if ((term->window = wayl_win_init(term, token)) == NULL) goto err; /* Load fonts */ if (!term_font_dpi_changed(term, 0.)) goto err; term->font_subpixel = get_font_subpixel(term); term_set_window_title(term, conf->title); /* Let the Wayland backend know we exist */ tll_push_back(wayl->terms, term); switch (conf->startup_mode) { case STARTUP_WINDOWED: break; case STARTUP_MAXIMIZED: xdg_toplevel_set_maximized(term->window->xdg_toplevel); break; case STARTUP_FULLSCREEN: xdg_toplevel_set_fullscreen(term->window->xdg_toplevel, NULL); break; } if (!initialize_render_workers(term)) goto err; return term; err: term->shutdown.in_progress = true; term_destroy(term); return NULL; close_fds: close(ptmx); fdm_del(fdm, flash_fd); fdm_del(fdm, delay_lower_fd); fdm_del(fdm, delay_upper_fd); fdm_del(fdm, app_sync_updates_fd); fdm_del(fdm, title_update_fd); fdm_del(fdm, app_id_update_fd); free(term); return NULL; } void term_window_configured(struct terminal *term) { /* Enable ptmx FDM callback */ if (!term->shutdown.in_progress) { xassert(term->window->is_configured); fdm_add(term->fdm, term->ptmx, EPOLLIN, &fdm_ptmx, term); } } /* * Shutdown logic * * A foot instance can be terminated in two ways: * * - the client application terminates (user types 'exit', or pressed C-d in the * shell, etc) * - the foot window is closed * * Both variants need to trigger to "other" action. I.e. if the client * application is terminated, then we need to close the window. If the window is * closed, we need to terminate the client application. * * Only when *both* tasks have completed do we consider ourselves fully * shutdown. This is when we can call term_destroy(), and the user provided * shutdown callback. * * The functions involved with this are: * * - shutdown_maybe_done(): called after any of the two tasks above have * completed. When it determines that *both* tasks are done, it calls * term_destroy() and the user provided shutdown callback. * * - fdm_client_terminated(): reaper callback, called when the client * application has terminated. * * + Kills the "terminate" timeout timer * + Calls shutdown_maybe_done() if the shutdown procedure has already * started (i.e. the window being closed initiated the shutdown) * -OR- * Initiates the shutdown itself, by calling term_shutdown() (client * application termination initiated the shutdown). * * - term_shutdown(): unregisters all FDM callbacks, sends SIGTERM to the client * application and installs a "terminate" timeout timer (if it hasn't already * terminated). Finally registers an event FD with the FDM, which is * immediately triggered. This is done to ensure any pending FDM events are * handled before shutting down. * * - fdm_shutdown(): FDM callback, triggered by the event FD in * term_shutdown(). Unmaps and destroys the window resources, and ensures the * seats' focused pointers don't reference us. Finally calls * shutdown_maybe_done(). * * - fdm_terminate_timeout(): FDM callback for the "terminate" timeout * timer. This function is called when the client application hasn't * terminated after 60 seconds (after the SIGTERM). Sends SIGKILL to the * client application. * * - term_destroy(): normally called from shutdown_maybe_done(), when both the * window has been unmapped, and the client application has terminated. In * this case, it simply destroys all resources. * * It may however also be called without term_shutdown() having been called * (typically in error code paths - for example, when the Wayland connection * is closed by the compositor). In this case, the client application is * typically still running, and we can't assume the FDM is running. To handle * this, we install configure a 60 second SIGALRM, send SIGTERM to the client * application, and then enter a blocking waitpid(). * * If the alarm triggers, we send SIGKILL and once again enter a blocking * waitpid(). */ static void shutdown_maybe_done(struct terminal *term) { bool shutdown_done = term->window == NULL && term->shutdown.client_has_terminated; LOG_DBG("window=%p, slave-has-been-reaped=%d --> %s", (void *)term->window, term->shutdown.client_has_terminated, (shutdown_done ? "shutdown done, calling term_destroy()" : "no action")); if (!shutdown_done) return; void (*cb)(void *, int) = term->shutdown.cb; void *cb_data = term->shutdown.cb_data; int exit_code = term_destroy(term); if (cb != NULL) cb(cb_data, exit_code); } static void fdm_client_terminated(struct reaper *reaper, pid_t pid, int status, void *data) { struct terminal *term = data; LOG_DBG("slave (PID=%u) died", pid); term->shutdown.client_has_terminated = true; term->shutdown.exit_status = status; if (term->shutdown.terminate_timeout_fd >= 0) { fdm_del(term->fdm, term->shutdown.terminate_timeout_fd); term->shutdown.terminate_timeout_fd = -1; } if (term->shutdown.in_progress) shutdown_maybe_done(term); else if (!term->conf->hold_at_exit) term_shutdown(term); } static bool fdm_shutdown(struct fdm *fdm, int fd, int events, void *data) { struct terminal *term = data; /* Kill the event FD */ fdm_del(term->fdm, fd); wayl_win_destroy(term->window); term->window = NULL; struct wayland *wayl = 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. */ tll_foreach(wayl->seats, it) { if (it->item.kbd_focus == term) it->item.kbd_focus = NULL; if (it->item.mouse_focus == term) it->item.mouse_focus = NULL; } shutdown_maybe_done(term); return true; } static bool fdm_terminate_timeout(struct fdm *fdm, int fd, int events, void *data) { uint64_t unused; ssize_t bytes = read(fd, &unused, sizeof(unused)); if (bytes < 0) { LOG_ERRNO("failed to read from slave terminate timeout FD"); return false; } struct terminal *term = data; xassert(!term->shutdown.client_has_terminated); LOG_DBG("slave (PID=%u) has not terminated, sending %s (%d)", term->slave, term->shutdown.next_signal == SIGTERM ? "SIGTERM" : term->shutdown.next_signal == SIGKILL ? "SIGKILL" : "", term->shutdown.next_signal); kill(-term->slave, term->shutdown.next_signal); switch (term->shutdown.next_signal) { case SIGTERM: term->shutdown.next_signal = SIGKILL; break; case SIGKILL: /* Disarm. Shouldn't be necessary, as we should be able to shutdown completely after sending SIGKILL, before the next timeout occurs). But lets play it safe... */ if (term->shutdown.terminate_timeout_fd >= 0) { timerfd_settime( term->shutdown.terminate_timeout_fd, 0, &(const struct itimerspec){0}, NULL); } break; default: BUG("can only handle SIGTERM and SIGKILL"); return false; } return true; } bool term_shutdown(struct terminal *term) { if (term->shutdown.in_progress) return true; term->shutdown.in_progress = true; /* * Close FDs then postpone self-destruction to the next poll * iteration, by creating an event FD that we trigger immediately. */ term_cursor_blink_update(term); xassert(term->cursor_blink.fd < 0); 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.title.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->blink.fd); fdm_del(term->fdm, term->flash.fd); del_utmp_record(term->conf, term->reaper, term->ptmx); if (term->window != NULL && term->window->is_configured) fdm_del(term->fdm, term->ptmx); else close(term->ptmx); if (!term->shutdown.client_has_terminated) { if (term->slave <= 0) { term->shutdown.client_has_terminated = true; } else { LOG_DBG("initiating asynchronous terminate of slave; " "sending SIGHUP to PID=%u", term->slave); kill(-term->slave, SIGHUP); /* * Set up a timer, with an interval - on the first timeout * we'll send SIGTERM. If the the client application still * isn't terminating, we'll wait an additional interval, * and then send SIGKILL. */ const struct itimerspec timeout = {.it_value = {.tv_sec = 30}, .it_interval = {.tv_sec = 30}}; int timeout_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK); if (timeout_fd < 0 || timerfd_settime(timeout_fd, 0, &timeout, NULL) < 0 || !fdm_add(term->fdm, timeout_fd, EPOLLIN, &fdm_terminate_timeout, term)) { if (timeout_fd >= 0) close(timeout_fd); LOG_ERRNO("failed to create slave terminate timeout FD"); return false; } xassert(term->shutdown.terminate_timeout_fd < 0); term->shutdown.terminate_timeout_fd = timeout_fd; term->shutdown.next_signal = SIGTERM; } } term->selection.auto_scroll.fd = -1; term->render.app_sync_updates.timer_fd = -1; term->render.app_id.timer_fd = -1; term->render.title.timer_fd = -1; term->delayed_render_timer.lower_fd = -1; term->delayed_render_timer.upper_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; } } del_utmp_record(term->conf, term->reaper, term->ptmx); 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.title.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->shutdown.terminate_timeout_fd >= 0) fdm_del(term->fdm, term->shutdown.terminate_timeout_fd); if (term->window != NULL) { wayl_win_destroy(term->window); term->window = NULL; } mtx_lock(&term->render.workers.lock); xassert(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; if (term->render.workers.threads != NULL) { 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); } } mtx_unlock(&term->render.workers.lock); key_binding_unref(term->wl->key_binding_manager, term->conf); urls_reset(term); free(term->vt.osc.data); free(term->vt.osc8.uri); composed_free(term->composed); free(term->app_id); 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++) fcft_destroy(term->fonts[i]); for (size_t i = 0; i < 4; i++) free(term->font_sizes[i]); free_custom_glyphs( &term->custom_glyphs.box_drawing, GLYPH_BOX_DRAWING_COUNT); free_custom_glyphs( &term->custom_glyphs.braille, GLYPH_BRAILLE_COUNT); free_custom_glyphs( &term->custom_glyphs.legacy, GLYPH_LEGACY_COUNT); free(term->search.buf); free(term->search.last.buf); if (term->render.workers.threads != NULL) { 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); mtx_destroy(&term->render.workers.lock); sem_destroy(&term->render.workers.start); sem_destroy(&term->render.workers.done); xassert(tll_length(term->render.workers.queue) == 0); tll_free(term->render.workers.queue); shm_unref(term->render.last_buf); shm_chain_free(term->render.chains.grid); shm_chain_free(term->render.chains.search); shm_chain_free(term->render.chains.scrollback_indicator); shm_chain_free(term->render.chains.render_timer); shm_chain_free(term->render.chains.url); shm_chain_free(term->render.chains.csd); shm_chain_free(term->render.chains.overlay); pixman_region32_fini(&term->render.last_overlay_clip); tll_free(term->tab_stops); tll_foreach(term->ptmx_buffers, it) { free(it->item.data); tll_remove(term->ptmx_buffers, it); } tll_foreach(term->ptmx_paste_buffers, it) { free(it->item.data); tll_remove(term->ptmx_paste_buffers, it); } notify_free(term, &term->kitty_notification); tll_foreach(term->active_notifications, it) { notify_free(term, &it->item); tll_remove(term->active_notifications, it); } for (size_t i = 0; i < ALEN(term->notification_icons); i++) notify_icon_free(&term->notification_icons[i]); sixel_fini(term); term_ime_reset(term); grid_free(&term->normal); grid_free(&term->alt); grid_free(term->interactive_resizing.grid); free(term->interactive_resizing.grid); free(term->foot_exe); free(term->cwd); free(term->mouse_user_cursor); free(term->color_stack.stack); int ret = EXIT_SUCCESS; if (term->slave > 0) { /* We'll deal with this explicitly */ reaper_del(term->reaper, term->slave); int exit_status; if (term->shutdown.client_has_terminated) exit_status = term->shutdown.exit_status; else { LOG_DBG("initiating blocking terminate of slave; " "sending SIGHUP to PID=%u", term->slave); kill(-term->slave, SIGHUP); /* * we've closed the ptxm, and sent SIGTERM to the client * application. It *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 30 * second alarm. If the slave hasn't died after this time, 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. */ struct sigaction action = {.sa_handler = &sig_alarm}; sigemptyset(&action.sa_mask); sigaction(SIGALRM, &action, NULL); /* Wait, then send SIGTERM, wait again, then send SIGKILL */ int next_signal = SIGTERM; alarm_raised = 0; alarm(30); while (true) { int r = waitpid(term->slave, &exit_status, 0); if (r == term->slave) break; if (r == -1) { xassert(errno == EINTR); if (alarm_raised) { LOG_DBG("slave (PID=%u) has not terminated yet, " "sending: %s (%d)", term->slave, next_signal == SIGTERM ? "SIGTERM" : "SIGKILL", next_signal); kill(-term->slave, next_signal); next_signal = SIGKILL; alarm_raised = 0; alarm(30); } } } /* Cancel alarm */ alarm(0); action.sa_handler = SIG_DFL; sigaction(SIGALRM, &action, NULL); } ret = EXIT_FAILURE; if (WIFEXITED(exit_status)) { ret = WEXITSTATUS(exit_status); LOG_DBG("slave exited with code %d", ret); } else if (WIFSIGNALED(exit_status)) { ret = WTERMSIG(exit_status); LOG_WARN("slave exited with signal %d (%s)", ret, strsignal(ret)); } else { LOG_WARN("slave exited for unknown reason (status = 0x%08x)", exit_status); } } free(term); #if defined(__GLIBC__) if (!malloc_trim(0)) LOG_WARN("failed to trim memory"); #endif return ret; } static inline void erase_cell_range(struct terminal *term, struct row *row, int start, int end) { xassert(start < term->cols); xassert(end < term->cols); row->dirty = true; const enum color_source bg_src = term->vt.attrs.bg_src; if (unlikely(bg_src != COLOR_DEFAULT)) { for (int col = start; col <= end; col++) { struct cell *c = &row->cells[col]; c->wc = 0; c->attrs = (struct attributes){.bg_src = bg_src, .bg = term->vt.attrs.bg}; } } else memset(&row->cells[start], 0, (end - start + 1) * sizeof(row->cells[0])); if (unlikely(row->extra != NULL)) { grid_row_uri_range_erase(row, start, end); grid_row_underline_range_erase(row, start, end); } } static inline void erase_line(struct terminal *term, struct row *row) { erase_cell_range(term, row, 0, term->cols - 1); row->linebreak = false; row->shell_integration.prompt_marker = false; row->shell_integration.cmd_start = -1; row->shell_integration.cmd_end = -1; } void term_reset(struct terminal *term, bool hard) { LOG_INFO("%s resetting the terminal", hard ? "hard" : "soft"); term->cursor_keys_mode = CURSOR_KEYS_NORMAL; term->keypad_keys_mode = KEYPAD_NUMERICAL; term->reverse = false; term->hide_cursor = false; term->reverse_wrap = true; term->auto_margin = true; term->insert_mode = false; term->bracketed_paste = false; term->focus_events = false; term->num_lock_modifier = true; term->bell_action_enabled = true; term->mouse_tracking = MOUSE_NONE; term->mouse_reporting = MOUSE_NORMAL; term->charsets.selected = G0; term->charsets.set[G0] = CHARSET_ASCII; term->charsets.set[G1] = CHARSET_ASCII; term->charsets.set[G2] = CHARSET_ASCII; term->charsets.set[G3] = CHARSET_ASCII; term->saved_charsets = term->charsets; tll_free_and_free(term->window_title_stack, free); term_set_window_title(term, term->conf->title); term_set_user_mouse_cursor(term, NULL); term->modify_other_keys_2 = false; memset(term->normal.kitty_kbd.flags, 0, sizeof(term->normal.kitty_kbd.flags)); memset(term->alt.kitty_kbd.flags, 0, sizeof(term->alt.kitty_kbd.flags)); term->normal.kitty_kbd.idx = term->alt.kitty_kbd.idx = 0; term->scroll_region.start = 0; term->scroll_region.end = term->rows; free(term->vt.osc8.uri); free(term->vt.osc.data); term->vt = (struct vt){ .state = 0, /* STATE_GROUND */ }; if (term->grid == &term->alt) { term->grid = &term->normal; selection_cancel(term); } term->meta.esc_prefix = true; term->meta.eight_bit = true; tll_foreach(term->normal.sixel_images, it) { sixel_destroy(&it->item); tll_remove(term->normal.sixel_images, it); } tll_foreach(term->alt.sixel_images, it) { sixel_destroy(&it->item); tll_remove(term->alt.sixel_images, it); } notify_free(term, &term->kitty_notification); tll_foreach(term->active_notifications, it) { notify_free(term, &it->item); tll_remove(term->active_notifications, it); } for (size_t i = 0; i < ALEN(term->notification_icons); i++) notify_icon_free(&term->notification_icons[i]); term->grapheme_shaping = term->conf->tweak.grapheme_shaping; #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED term_ime_enable(term); #endif term->bits_affecting_ascii_printer.value = 0; term_update_ascii_printer(term); if (!hard) return; term->flash.active = false; term->blink.state = BLINK_ON; fdm_del(term->fdm, term->blink.fd); term->blink.fd = -1; term->colors.fg = term->conf->colors.fg; term->colors.bg = term->conf->colors.bg; term->colors.alpha = term->conf->colors.alpha; term->colors.cursor_fg = term->conf->cursor.color.text; term->colors.cursor_bg = term->conf->cursor.color.cursor; term->colors.selection_fg = term->conf->colors.selection_fg; term->colors.selection_bg = term->conf->colors.selection_bg; term->colors.use_custom_selection = term->conf->colors.use_custom.selection; memcpy(term->colors.table, term->conf->colors.table, sizeof(term->colors.table)); free(term->color_stack.stack); term->color_stack.stack = NULL; term->color_stack.size = 0; term->color_stack.idx = 0; term->origin = ORIGIN_ABSOLUTE; term->normal.cursor.lcf = false; term->alt.cursor.lcf = false; term->normal.cursor = (struct cursor){.point = {0, 0}}; term->normal.saved_cursor = (struct cursor){.point = {0, 0}}; term->alt.cursor = (struct cursor){.point = {0, 0}}; term->alt.saved_cursor = (struct cursor){.point = {0, 0}}; term->cursor_style = term->conf->cursor.style; term->cursor_blink.decset = false; term->cursor_blink.deccsusr = term->conf->cursor.blink.enabled; term_cursor_blink_update(term); 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++) { struct row *r = grid_row_and_alloc(&term->normal, i); erase_line(term, r); } for (size_t i = 0; i < term->rows; i++) { struct row *r = grid_row_and_alloc(&term->alt, i); erase_line(term, r); } 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.scroll_damage); tll_free(term->alt.scroll_damage); term->render.last_cursor.row = NULL; term_damage_all(term); term->sixel.scrolling = true; term->sixel.cursor_right_of_graphics = false; term->sixel.use_private_palette = true; term->sixel.max_width = SIXEL_MAX_WIDTH; term->sixel.max_height = SIXEL_MAX_HEIGHT; term->sixel.palette_size = SIXEL_MAX_COLORS; free(term->sixel.private_palette); free(term->sixel.shared_palette); term->sixel.private_palette = term->sixel.shared_palette = NULL; } static bool term_font_size_adjust_by_points(struct terminal *term, float amount) { const struct config *conf = term->conf; const float dpi = term->font_is_sized_by_dpi ? term->font_dpi : 96.; for (size_t i = 0; i < 4; i++) { const struct config_font_list *font_list = &conf->fonts[i]; for (size_t j = 0; j < font_list->count; j++) { struct config_font *font = &term->font_sizes[i][j]; float old_pt_size = font->pt_size; if (font->px_size > 0) old_pt_size = font->px_size * 72. / dpi; font->pt_size = fmaxf(old_pt_size + amount, 0.); font->px_size = -1; } } return reload_fonts(term, true); } static bool term_font_size_adjust_by_pixels(struct terminal *term, int amount) { const struct config *conf = term->conf; const float dpi = term->font_is_sized_by_dpi ? term->font_dpi : 96.; for (size_t i = 0; i < 4; i++) { const struct config_font_list *font_list = &conf->fonts[i]; for (size_t j = 0; j < font_list->count; j++) { struct config_font *font = &term->font_sizes[i][j]; int old_px_size = font->px_size; if (font->px_size <= 0) old_px_size = font->pt_size * dpi / 72.; font->px_size = max(old_px_size + amount, 1); } } return reload_fonts(term, true); } static bool term_font_size_adjust_by_percent(struct terminal *term, bool increment, float percent) { const struct config *conf = term->conf; const float multiplier = increment ? 1. + percent : 1. / (1. + percent); for (size_t i = 0; i < 4; i++) { const struct config_font_list *font_list = &conf->fonts[i]; for (size_t j = 0; j < font_list->count; j++) { struct config_font *font = &term->font_sizes[i][j]; if (font->px_size > 0) font->px_size = max(font->px_size * multiplier, 1); else font->pt_size = fmax(font->pt_size * multiplier, 0); } } return reload_fonts(term, true); } bool term_font_size_increase(struct terminal *term) { const struct config *conf = term->conf; const struct font_size_adjustment *inc_dec = &conf->font_size_adjustment; if (inc_dec->percent > 0.) return term_font_size_adjust_by_percent(term, true, inc_dec->percent); else if (inc_dec->pt_or_px.px > 0) return term_font_size_adjust_by_pixels(term, inc_dec->pt_or_px.px); else return term_font_size_adjust_by_points(term, inc_dec->pt_or_px.pt); } bool term_font_size_decrease(struct terminal *term) { const struct config *conf = term->conf; const struct font_size_adjustment *inc_dec = &conf->font_size_adjustment; if (inc_dec->percent > 0.) return term_font_size_adjust_by_percent(term, false, inc_dec->percent); else if (inc_dec->pt_or_px.px > 0) return term_font_size_adjust_by_pixels(term, -inc_dec->pt_or_px.px); else return term_font_size_adjust_by_points(term, -inc_dec->pt_or_px.pt); } bool term_font_size_reset(struct terminal *term) { return load_fonts_from_conf(term); } bool term_fractional_scaling(const struct terminal *term) { return term->wl->fractional_scale_manager != NULL && term->wl->viewporter != NULL && term->window->scale > 0.; } bool term_preferred_buffer_scale(const struct terminal *term) { return term->window->preferred_buffer_scale > 0; } bool term_update_scale(struct terminal *term) { const struct wl_window *win = term->window; /* * We have a number of "sources" we can use as scale. We choose * the scale in the following order: * * - "preferred" scale, from the fractional-scale-v1 protocol * - "preferred" scale, from wl_compositor version 6. NOTE: if the compositor advertises version 6 we must use 1.0 until wl_surface.preferred_buffer_scale is sent * - scaling factor of output we most recently were mapped on * - if we're not mapped, use the last known scaling factor * - if we're not mapped, and we don't have a last known scaling * factor, use the scaling factor from the first available * output. * - if there aren't any outputs available, use 1.0 */ const float new_scale = (term_fractional_scaling(term) ? win->scale : term_preferred_buffer_scale(term) ? win->preferred_buffer_scale : tll_length(win->on_outputs) > 0 ? tll_back(win->on_outputs)->scale : term->scale_before_unmap > 0. ? term->scale_before_unmap : tll_length(term->wl->monitors) > 0 ? tll_front(term->wl->monitors).scale : 1.); if (new_scale == term->scale) return false; LOG_DBG("scaling factor changed: %.2f -> %.2f", term->scale, new_scale); term->scale_before_unmap = new_scale; term->scale = new_scale; return true; } bool term_font_dpi_changed(struct terminal *term, float old_scale) { float dpi = get_font_dpi(term); xassert(term->scale > 0.); bool was_scaled_using_dpi = term->font_is_sized_by_dpi; bool will_scale_using_dpi = term->conf->dpi_aware; bool need_font_reload = was_scaled_using_dpi != will_scale_using_dpi || (will_scale_using_dpi ? term->font_dpi != dpi : old_scale != term->scale); if (need_font_reload) { LOG_DBG("DPI/scale change: DPI-aware=%s, " "DPI: %.2f -> %.2f, scale: %.2f -> %.2f, " "sizing font based on monitor's %s", term->conf->dpi_aware ? "yes" : "no", term->font_dpi, dpi, old_scale, term->scale, will_scale_using_dpi ? "DPI" : "scaling factor"); } term->font_dpi = dpi; term->font_dpi_before_unmap = dpi; term->font_is_sized_by_dpi = will_scale_using_dpi; if (!need_font_reload) return false; return reload_fonts(term, false); } void term_font_subpixel_changed(struct terminal *term) { enum fcft_subpixel subpixel = get_font_subpixel(term); if (term->font_subpixel == subpixel) return; #if defined(_DEBUG) && LOG_ENABLE_DBG static const char *const str[] = { [FCFT_SUBPIXEL_DEFAULT] = "default", [FCFT_SUBPIXEL_NONE] = "disabled", [FCFT_SUBPIXEL_HORIZONTAL_RGB] = "RGB", [FCFT_SUBPIXEL_HORIZONTAL_BGR] = "BGR", [FCFT_SUBPIXEL_VERTICAL_RGB] = "V-RGB", [FCFT_SUBPIXEL_VERTICAL_BGR] = "V-BGR", }; LOG_DBG("subpixel mode changed: %s -> %s", str[term->font_subpixel], str[subpixel]); #endif term->font_subpixel = subpixel; term_damage_view(term); render_refresh(term); } int term_font_baseline(const struct terminal *term) { const struct fcft_font *font = term->fonts[0]; const int line_height = term->cell_height; const int font_height = font->ascent + font->descent; /* * Center glyph on the line *if* using a custom line height, * otherwise the baseline is simply 'descent' pixels above the * bottom of the cell */ const int glyph_top_y = term->font_line_height.px >= 0 ? round((line_height - font_height) / 2.) : 0; return term->font_y_ofs + line_height - glyph_top_y - font->descent; } void term_damage_rows(struct terminal *term, int start, int end) { xassert(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) { xassert(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_cursor(struct terminal *term) { term->grid->cur_row->cells[term->grid->cursor.point.col].attrs.clean = 0; term->grid->cur_row->dirty = true; } void term_damage_margins(struct terminal *term) { term->render.margins = true; } void term_damage_color(struct terminal *term, enum color_source src, int idx) { xassert(src == COLOR_DEFAULT || src == COLOR_BASE256); for (int r = 0; r < term->rows; r++) { struct row *row = grid_row_in_view(term->grid, r); struct cell *cell = &row->cells[0]; const struct cell *end = &row->cells[term->cols]; for (; cell < end; cell++) { bool dirty = false; switch (cell->attrs.fg_src) { case COLOR_BASE16: case COLOR_BASE256: if (src == COLOR_BASE256 && cell->attrs.fg == idx) dirty = true; break; case COLOR_DEFAULT: if (src == COLOR_DEFAULT) { /* Doesn't matter whether we've updated the default foreground, or background, we still want to dirty this cell, to be sure we handle all cases of color inversion/reversal */ dirty = true; } break; case COLOR_RGB: /* Not affected */ break; } switch (cell->attrs.bg_src) { case COLOR_BASE16: case COLOR_BASE256: if (src == COLOR_BASE256 && cell->attrs.bg == idx) dirty = true; break; case COLOR_DEFAULT: if (src == COLOR_DEFAULT) { /* Doesn't matter whether we've updated the default foreground, or background, we still want to dirty this cell, to be sure we handle all cases of color inversion/reversal */ dirty = true; } break; case COLOR_RGB: /* Not affected */ break; } if (dirty) { cell->attrs.clean = 0; row->dirty = true; } } /* Colored underlines */ if (row->extra != NULL) { const struct row_ranges *underlines = &row->extra->underline_ranges; for (int i = 0; i < underlines->count; i++) { const struct row_range *range = &underlines->v[i]; /* Underline colors are either default, or BASE256/RGB, but never BASE16 */ xassert(range->underline.color_src == COLOR_DEFAULT || range->underline.color_src == COLOR_BASE256 || range->underline.color_src == COLOR_RGB); if (range->underline.color_src == src) { struct cell *c = &row->cells[range->start]; const struct cell *e = &row->cells[range->end + 1]; for (; c < e; c++) c->attrs.clean = 0; row->dirty = true; } } } } } void term_damage_scroll(struct terminal *term, enum damage_type damage_type, struct scroll_region region, int lines) { if (likely(tll_length(term->grid->scroll_damage) > 0)) { struct damage *dmg = &tll_back(term->grid->scroll_damage); if (likely( dmg->type == damage_type && dmg->region.start == region.start && dmg->region.end == region.end)) { /* Make sure we don't overflow... */ int new_line_count = (int)dmg->lines + lines; if (likely(new_line_count <= UINT16_MAX)) { dmg->lines = new_line_count; return; } } } struct damage dmg = { .type = damage_type, .region = region, .lines = lines, }; tll_push_back(term->grid->scroll_damage, dmg); } void term_erase(struct terminal *term, int start_row, int start_col, int end_row, int end_col) { xassert(start_row <= end_row); xassert(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); sixel_overwrite_by_row(term, start_row, start_col, end_col - start_col + 1); return; } xassert(end_row > start_row); erase_cell_range( term, grid_row(term->grid, start_row), start_col, term->cols - 1); sixel_overwrite_by_row(term, start_row, start_col, term->cols - start_col); for (int r = start_row + 1; r < end_row; r++) erase_line(term, grid_row(term->grid, r)); sixel_overwrite_by_rectangle( term, start_row + 1, 0, end_row - start_row, term->cols); erase_cell_range(term, grid_row(term->grid, end_row), 0, end_col); sixel_overwrite_by_row(term, end_row, 0, end_col + 1); } void term_erase_scrollback(struct terminal *term) { const struct grid *grid = term->grid; const int num_rows = grid->num_rows; const int mask = num_rows - 1; const int scrollback_history_size = num_rows - term->rows; if (scrollback_history_size == 0) return; const int start = (grid->offset + term->rows) & mask; const int end = (grid->offset - 1) & mask; const int rel_start = grid_row_abs_to_sb(grid, term->rows, start); const int rel_end = grid_row_abs_to_sb(grid, term->rows, end); const int sel_start = selection_get_start(term).row; const int sel_end = selection_get_end(term).row; if (sel_end >= 0) { /* * Cancel selection if it touches any of the rows in the * scrollback, since we can't have the selection reference * soon-to-be deleted rows. * * This is done by range checking the selection range against * the scrollback range. * * To make this comparison simpler, the start/end absolute row * numbers are "rebased" against the scrollback start, where * row 0 is the *first* row in the scrollback. A high number * thus means the row is further *down* in the scrollback, * closer to the screen bottom. */ const int rel_sel_start = grid_row_abs_to_sb(grid, term->rows, sel_start); const int rel_sel_end = grid_row_abs_to_sb(grid, term->rows, sel_end); if ((rel_sel_start <= rel_start && rel_sel_end >= rel_start) || (rel_sel_start <= rel_end && rel_sel_end >= rel_end) || (rel_sel_start >= rel_start && rel_sel_end <= rel_end)) { selection_cancel(term); } } tll_foreach(term->grid->sixel_images, it) { struct sixel *six = &it->item; const int six_start = grid_row_abs_to_sb(grid, term->rows, six->pos.row); const int six_end = grid_row_abs_to_sb( grid, term->rows, six->pos.row + six->rows - 1); if ((six_start <= rel_start && six_end >= rel_start) || (six_start <= rel_end && six_end >= rel_end) || (six_start >= rel_start && six_end <= rel_end)) { sixel_destroy(six); tll_remove(term->grid->sixel_images, it); } } for (int i = start;; i = (i + 1) & mask) { struct row *row = term->grid->rows[i]; if (row != NULL) { if (term->render.last_cursor.row == row) term->render.last_cursor.row = NULL; grid_row_free(row); term->grid->rows[i] = NULL; } if (i == end) break; } term->grid->view = term->grid->offset; #if defined(_DEBUG) for (int i = 0; i < term->rows; i++) { xassert(grid_row_in_view(term->grid, i) != NULL); } #endif term_damage_view(term); } UNITTEST { const int scrollback_rows = 16; const int term_rows = 5; const int cols = 5; struct fdm *fdm = fdm_init(); xassert(fdm != NULL); struct terminal term = { .fdm = fdm, .rows = term_rows, .cols = cols, .normal = { .rows = xcalloc(scrollback_rows, sizeof(term.normal.rows[0])), .num_rows = scrollback_rows, .num_cols = cols, }, .grid = &term.normal, .selection = { .coords = { .start = {-1, -1}, .end = {-1, -1}, }, .kind = SELECTION_NONE, .auto_scroll = { .fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK), }, }, }; xassert(term.selection.auto_scroll.fd >= 0); #define populate_scrollback() do { \ for (int i = 0; i < scrollback_rows; i++) { \ if (term.normal.rows[i] == NULL) { \ struct row *r = xcalloc(1, sizeof(*term.normal.rows[i])); \ r->cells = xcalloc(cols, sizeof(r->cells[0])); \ term.normal.rows[i] = r; \ } \ } \ } while (0) /* * Test case 1 - no selection, just verify all rows except those * on screen have been deleted. */ populate_scrollback(); term.normal.offset = 11; term_erase_scrollback(&term); for (int i = 0; i < scrollback_rows; i++) { if (i >= term.normal.offset && i < term.normal.offset + term_rows) xassert(term.normal.rows[i] != NULL); else xassert(term.normal.rows[i] == NULL); } /* * Test case 2 - selection that touches the scrollback. Verify the * selection is cancelled. */ term.normal.offset = 14; /* Screen covers rows 14,15,0,1,2 */ /* Selection covers rows 15,0,1,2,3 */ term.selection.coords.start = (struct coord){.row = 15}; term.selection.coords.end = (struct coord){.row = 19}; term.selection.kind = SELECTION_CHAR_WISE; populate_scrollback(); term_erase_scrollback(&term); xassert(term.selection.coords.start.row < 0); xassert(term.selection.coords.end.row < 0); xassert(term.selection.kind == SELECTION_NONE); /* * Test case 3 - selection that does *not* touch the * scrollback. Verify the selection is *not* cancelled. */ /* Selection covers rows 15,0 */ term.selection.coords.start = (struct coord){.row = 15}; term.selection.coords.end = (struct coord){.row = 16}; term.selection.kind = SELECTION_CHAR_WISE; populate_scrollback(); term_erase_scrollback(&term); xassert(term.selection.coords.start.row == 15); xassert(term.selection.coords.end.row == 16); xassert(term.selection.kind == SELECTION_CHAR_WISE); term.selection.coords.start = (struct coord){-1, -1}; term.selection.coords.end = (struct coord){-1, -1}; term.selection.kind = SELECTION_NONE; /* * Test case 4 - sixel that touch the scrollback */ struct sixel six = { .rows = 5, .pos = { .row = 15, }, }; tll_push_back(term.normal.sixel_images, six); populate_scrollback(); term_erase_scrollback(&term); xassert(tll_length(term.normal.sixel_images) == 0); /* * Test case 5 - sixel that does *not* touch the scrollback */ six.rows = 3; tll_push_back(term.normal.sixel_images, six); populate_scrollback(); term_erase_scrollback(&term); xassert(tll_length(term.normal.sixel_images) == 1); /* Cleanup */ tll_free(term.normal.sixel_images); close(term.selection.auto_scroll.fd); for (int i = 0; i < scrollback_rows; i++) grid_row_free(term.normal.rows[i]); free(term.normal.rows); fdm_destroy(fdm); } 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); } BUG("Invalid cursor_origin value"); return -1; } void term_cursor_to(struct terminal *term, int row, int col) { xassert(row < term->rows); xassert(col < term->cols); term->grid->cursor.lcf = false; term->grid->cursor.point.col = col; term->grid->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_col(struct terminal *term, int col) { xassert(col < term->cols); term->grid->cursor.lcf = false; term->grid->cursor.point.col = col; } void term_cursor_left(struct terminal *term, int count) { int move_amount = min(term->grid->cursor.point.col, count); term->grid->cursor.point.col -= move_amount; xassert(term->grid->cursor.point.col >= 0); term->grid->cursor.lcf = false; } void term_cursor_right(struct terminal *term, int count) { int move_amount = min(term->cols - term->grid->cursor.point.col - 1, count); term->grid->cursor.point.col += move_amount; xassert(term->grid->cursor.point.col < term->cols); term->grid->cursor.lcf = false; } void term_cursor_up(struct terminal *term, int count) { int top = term->origin == ORIGIN_ABSOLUTE ? 0 : term->scroll_region.start; xassert(term->grid->cursor.point.row >= top); int move_amount = min(term->grid->cursor.point.row - top, count); term_cursor_to(term, term->grid->cursor.point.row - move_amount, term->grid->cursor.point.col); } void term_cursor_down(struct terminal *term, int count) { int bottom = term->origin == ORIGIN_ABSOLUTE ? term->rows : term->scroll_region.end; xassert(bottom >= term->grid->cursor.point.row); int move_amount = min(bottom - term->grid->cursor.point.row - 1, count); term_cursor_to(term, term->grid->cursor.point.row + move_amount, term->grid->cursor.point.col); } static bool cursor_blink_rearm_timer(struct terminal *term) { if (term->cursor_blink.fd < 0) { int fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK); if (fd < 0) { LOG_ERRNO("failed to create cursor blink timer FD"); return false; } if (!fdm_add(term->fdm, fd, EPOLLIN, &fdm_cursor_blink, term)) { close(fd); return false; } term->cursor_blink.fd = fd; } const int rate_ms = term->conf->cursor.blink.rate_ms; const long secs = rate_ms / 1000; const long nsecs = (rate_ms % 1000) * 1000000; const struct itimerspec timer = { .it_value = {.tv_sec = secs, .tv_nsec = nsecs}, .it_interval = {.tv_sec = secs, .tv_nsec = nsecs}, }; if (timerfd_settime(term->cursor_blink.fd, 0, &timer, NULL) < 0) { LOG_ERRNO("failed to arm cursor blink timer"); fdm_del(term->fdm, term->cursor_blink.fd); term->cursor_blink.fd = -1; return false; } return true; } static bool cursor_blink_disarm_timer(struct terminal *term) { fdm_del(term->fdm, term->cursor_blink.fd); term->cursor_blink.fd = -1; return true; } void term_cursor_blink_update(struct terminal *term) { bool enable = term->cursor_blink.decset || term->cursor_blink.deccsusr; bool activate = !term->shutdown.in_progress && enable && term->visual_focus; LOG_DBG("decset=%d, deccsrusr=%d, focus=%d, shutting-down=%d, enable=%d, activate=%d", term->cursor_blink.decset, term->cursor_blink.deccsusr, term->visual_focus, term->shutdown.in_progress, enable, activate); if (activate && term->cursor_blink.fd < 0) { term->cursor_blink.state = CURSOR_BLINK_ON; cursor_blink_rearm_timer(term); } else if (!activate && term->cursor_blink.fd >= 0) cursor_blink_disarm_timer(term); } static bool selection_on_top_region(const struct terminal *term, struct scroll_region region) { return region.start > 0 && selection_on_rows(term, 0, region.start - 1); } static bool selection_on_bottom_region(const struct terminal *term, struct scroll_region region) { return region.end < term->rows && selection_on_rows(term, region.end, term->rows - 1); } 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); /* Verify scroll amount has been clamped */ xassert(rows <= region.end - region.start); /* Cancel selections that cannot be scrolled */ if (unlikely(term->selection.coords.end.row >= 0)) { /* * Selection is (partly) inside either the top or bottom * scrolling regions, or on (at least one) of the lines * scrolled in (i.e. reused lines). */ if (selection_on_top_region(term, region) || selection_on_bottom_region(term, region)) { selection_cancel(term); } else selection_scroll_up(term, rows); } sixel_scroll_up(term, rows); /* How many lines from the scrollback start is the current viewport? */ int view_sb_start_distance = grid_row_abs_to_sb( term->grid, term->rows, term->grid->view); bool view_follows = term->grid->view == term->grid->offset; term->grid->offset += rows; term->grid->offset &= term->grid->num_rows - 1; if (likely(view_follows)) { term_damage_scroll(term, DAMAGE_SCROLL, region, rows); selection_view_down(term, term->grid->offset); term->grid->view = term->grid->offset; } else if (unlikely(rows > view_sb_start_distance)) { /* Part of current view is being scrolled out */ int new_view = grid_row_sb_to_abs(term->grid, term->rows, 0); selection_view_down(term, new_view); cmd_scrollback_down(term, rows - view_sb_start_distance); } /* Top non-scrolling region. */ for (int i = region.start - 1; i >= 0; i--) grid_swap_row(term->grid, i - rows, i); /* Bottom non-scrolling region */ for (int i = term->rows - 1; i >= region.end; i--) grid_swap_row(term->grid, i - rows, i); /* Erase scrolled in lines */ for (int r = region.end - rows; r < region.end; r++) { struct row *row = grid_row_and_alloc(term->grid, r); erase_line(term, row); } term->grid->cur_row = grid_row(term->grid, term->grid->cursor.point.row); #if defined(_DEBUG) for (int r = 0; r < term->rows; r++) xassert(grid_row(term->grid, r) != NULL); #endif } 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); /* Verify scroll amount has been clamped */ xassert(rows <= region.end - region.start); /* Cancel selections that cannot be scrolled */ if (unlikely(term->selection.coords.end.row >= 0)) { /* * Selection is (partly) inside either the top or bottom * scrolling regions, or on (at least one) of the lines * scrolled in (i.e. reused lines). */ if (selection_on_top_region(term, region) || selection_on_bottom_region(term, region)) { selection_cancel(term); } else selection_scroll_down(term, rows); } /* Unallocate scrolled out lines */ for (int r = region.end - rows; r < region.end; r++) { const int abs_r = grid_row_absolute(term->grid, r); struct row *row = term->grid->rows[abs_r]; grid_row_free(row); term->grid->rows[abs_r] = NULL; if (term->render.last_cursor.row == row) term->render.last_cursor.row = NULL; } sixel_scroll_down(term, rows); bool view_follows = term->grid->view == term->grid->offset; term->grid->offset -= rows; term->grid->offset += term->grid->num_rows; term->grid->offset &= term->grid->num_rows - 1; xassert(term->grid->offset >= 0); xassert(term->grid->offset < term->grid->num_rows); if (view_follows) { term_damage_scroll(term, DAMAGE_SCROLL_REVERSE, region, rows); selection_view_up(term, term->grid->offset); 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); /* Top non-scrolling region */ for (int i = 0 + rows; i < region.start + rows; i++) grid_swap_row(term->grid, i, i - rows); /* Erase scrolled in lines */ for (int r = region.start; r < region.start + rows; r++) { struct row *row = grid_row_and_alloc(term->grid, r); erase_line(term, row); } term->grid->cur_row = grid_row(term->grid, term->grid->cursor.point.row); #if defined(_DEBUG) for (int r = 0; r < term->rows; r++) xassert(grid_row(term->grid, r) != NULL); #endif } void term_scroll_reverse(struct terminal *term, int rows) { term_scroll_reverse_partial(term, term->scroll_region, rows); } void term_carriage_return(struct terminal *term) { term_cursor_left(term, term->grid->cursor.point.col); } void term_linefeed(struct terminal *term) { term->grid->cur_row->linebreak = true; term->grid->cursor.lcf = false; if (term->grid->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->grid->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_save_cursor(struct terminal *term) { term->grid->saved_cursor = term->grid->cursor; term->vt.saved_attrs = term->vt.attrs; term->saved_charsets = term->charsets; } void term_restore_cursor(struct terminal *term, const struct cursor *cursor) { int row = min(cursor->point.row, term->rows - 1); int col = min(cursor->point.col, term->cols - 1); term_cursor_to(term, row, col); term->grid->cursor.lcf = cursor->lcf; term->vt.attrs = term->vt.saved_attrs; term->charsets = term->saved_charsets; term->bits_affecting_ascii_printer.charset = term->charsets.set[term->charsets.selected] != CHARSET_ASCII; term_update_ascii_printer(term); } void term_visual_focus_in(struct terminal *term) { if (term->visual_focus) return; term->visual_focus = true; term_cursor_blink_update(term); render_refresh_csd(term); } void term_visual_focus_out(struct terminal *term) { if (!term->visual_focus) return; term->visual_focus = false; term_cursor_blink_update(term); render_refresh_csd(term); } void term_kbd_focus_in(struct terminal *term) { if (term->kbd_focus) return; term->kbd_focus = true; if (term->render.urgency) { term->render.urgency = false; term_damage_margins(term); } cursor_refresh(term); if (term->focus_events) term_to_slave(term, "\033[I", 3); } void term_kbd_focus_out(struct terminal *term) { if (!term->kbd_focus) return; tll_foreach(term->wl->seats, it) if (it->item.kbd_focus == term) return; #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED if (term_ime_reset(term)) render_refresh(term); #endif term->kbd_focus = false; cursor_refresh(term); if (term->focus_events) term_to_slave(term, "\033[O", 3); } static int linux_mouse_button_to_x(int button) { /* Note: on X11, scroll events where reported as buttons. Not so * on Wayland. We manually map scroll events to custom "button" * defines (BTN_WHEEL_*). */ switch (button) { case BTN_LEFT: return 1; case BTN_MIDDLE: return 2; case BTN_RIGHT: return 3; case BTN_WHEEL_BACK: return 4; /* Foot custom define */ case BTN_WHEEL_FORWARD: return 5; /* Foot custom define */ case BTN_WHEEL_LEFT: return 6; /* Foot custom define */ case BTN_WHEEL_RIGHT: return 7; /* Foot custom define */ case BTN_SIDE: return 8; case BTN_EXTRA: return 9; case BTN_FORWARD: return 10; case BTN_BACK: return 11; case BTN_TASK: return 12; /* Guessing... */ 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: case 6: case 7: /* Like button 1 and 2, but with 64 added */ return xbutton - 4 + 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, int row_pixels, int col_pixels, 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_SGR_PIXELS: snprintf(response, sizeof(response), "\033[<%d;%d;%d%c", encoded_button, col_pixels + 1, row_pixels + 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, int row_pixels, int col_pixels) { report_mouse_click(term, encoded_button, row, col, row_pixels, col_pixels, false); } bool term_mouse_grabbed(const struct terminal *term, const struct seat *seat) { /* * Mouse is grabbed by us, regardless of whether mouse tracking * has been enabled or not. */ xkb_mod_mask_t mods; get_current_modifiers(seat, &mods, NULL, 0, true); const struct key_binding_set *bindings = key_binding_for(term->wl->key_binding_manager, term->conf, seat); const xkb_mod_mask_t override_modmask = bindings->selection_overrides; bool override_mods_pressed = (mods & override_modmask) == override_modmask; return term->mouse_tracking == MOUSE_NONE || (seat->kbd_focus == term && override_mods_pressed); } void term_mouse_down(struct terminal *term, int button, int row, int col, int row_pixels, int col_pixels, bool _shift, bool _alt, bool _ctrl) { /* 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->kbd_focus; bool shift = has_focus ? _shift : false; bool alt = has_focus ? _alt : false; bool ctrl = has_focus ? _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, row_pixels, col_pixels, false); break; case MOUSE_X10: /* Never enabled */ BUG("X10 mouse mode not implemented"); break; } } void term_mouse_up(struct terminal *term, int button, int row, int col, int row_pixels, int col_pixels, bool _shift, bool _alt, bool _ctrl) { /* 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 vertical scroll wheel buttons */ return; } int encoded = encode_xbutton(xbutton); if (encoded == -1) return; bool has_focus = term->kbd_focus; bool shift = has_focus ? _shift : false; bool alt = has_focus ? _alt : false; bool ctrl = has_focus ? _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, row_pixels, col_pixels, true); break; case MOUSE_X10: /* Never enabled */ BUG("X10 mouse mode not implemented"); break; } } void term_mouse_motion(struct terminal *term, int button, int row, int col, int row_pixels, int col_pixels, bool _shift, bool _alt, bool _ctrl) { 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->kbd_focus; bool shift = has_focus ? _shift : false; bool alt = has_focus ? _alt : false; bool ctrl = has_focus ? _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, row_pixels, col_pixels); break; case MOUSE_X10: /* Never enabled */ BUG("X10 mouse mode not implemented"); break; } } void term_xcursor_update_for_seat(struct terminal *term, struct seat *seat) { enum cursor_shape shape = CURSOR_SHAPE_NONE; switch (term->active_surface) { case TERM_SURF_GRID: if (seat->pointer.hidden) shape = CURSOR_SHAPE_HIDDEN; else if (cursor_string_to_server_shape(term->mouse_user_cursor) != 0 || render_xcursor_is_valid(seat, term->mouse_user_cursor)) { shape = CURSOR_SHAPE_CUSTOM; } else if (term_mouse_grabbed(term, seat)) { shape = CURSOR_SHAPE_TEXT; } else shape = CURSOR_SHAPE_LEFT_PTR; break; case TERM_SURF_TITLE: case TERM_SURF_BUTTON_MINIMIZE: case TERM_SURF_BUTTON_MAXIMIZE: case TERM_SURF_BUTTON_CLOSE: shape = CURSOR_SHAPE_LEFT_PTR; break; case TERM_SURF_BORDER_LEFT: case TERM_SURF_BORDER_RIGHT: case TERM_SURF_BORDER_TOP: case TERM_SURF_BORDER_BOTTOM: shape = xcursor_for_csd_border(term, seat->mouse.x, seat->mouse.y); break; case TERM_SURF_NONE: return; } if (shape == CURSOR_SHAPE_NONE) BUG("xcursor not set"); render_xcursor_set(seat, term, shape); } void term_xcursor_update(struct terminal *term) { tll_foreach(term->wl->seats, it) term_xcursor_update_for_seat(term, &it->item); } void term_set_window_title(struct terminal *term, const char *title) { if (term->conf->locked_title && term->window_title_has_been_set) return; if (term->window_title != NULL && streq(term->window_title, title)) return; if (mbsntoc32(NULL, title, strlen(title), 0) == (char32_t)-1) { /* 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); return; } free(term->window_title); term->window_title = xstrdup(title); render_refresh_title(term); term->window_title_has_been_set = true; } void term_set_app_id(struct terminal *term, const char *app_id) { if (app_id != NULL && *app_id == '\0') 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) return; free(term->app_id); if (app_id != NULL) { term->app_id = xstrdup(app_id); } else { term->app_id = NULL; } render_refresh_app_id(term); } 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; } } void term_bell(struct terminal *term) { if (!term->bell_action_enabled) return; if (term->conf->bell.urgent && !term->kbd_focus) { if (!wayl_win_set_urgent(term->window)) { /* * Urgency (xdg-activation) is relatively new in * Wayland. Fallback to our old, "faked", urgency - * rendering our window margins in red */ term->render.urgency = true; term_damage_margins(term); } } if (term->conf->bell.notify) { notify_notify(term, &(struct notification){ .title = (char *)"Bell", .body = (char *)"Bell in terminal", .expire_time = -1, }); } if (term->conf->bell.flash) term_flash(term, 100); if ((term->conf->bell.command.argv.args != NULL) && (!term->kbd_focus || term->conf->bell.command_focused)) { int devnull = open("/dev/null", O_RDONLY); spawn(term->reaper, NULL, term->conf->bell.command.argv.args, devnull, -1, -1, NULL, NULL, NULL); if (devnull >= 0) close(devnull); } } bool term_spawn_new(const struct terminal *term) { return spawn( term->reaper, term->cwd, (char *const []){term->foot_exe, NULL}, -1, -1, -1, NULL, NULL, NULL) >= 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"); } /* Disable pending refresh *iff* the grid is the *only* thing * scheduled to be re-rendered */ if (!term->render.refresh.csd && !term->render.refresh.search && !term->render.pending.csd && !term->render.pending.search) { term->render.refresh.grid = false; term->render.pending.grid = false; } /* 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); term->delayed_render_timer.is_armed = false; } void term_disable_app_sync_updates(struct terminal *term) { if (!term->render.app_sync_updates.enabled) return; term->render.app_sync_updates.enabled = false; render_refresh(term); /* Reset timers */ timerfd_settime( term->render.app_sync_updates.timer_fd, 0, &(struct itimerspec){{0}}, NULL); } static inline void print_linewrap(struct terminal *term) { if (likely(!term->grid->cursor.lcf)) { /* Not and end of line */ return; } if (unlikely(!term->auto_margin)) { /* Auto-wrap disabled */ return; } term->grid->cur_row->linebreak = false; term->grid->cursor.lcf = false; const int row = term->grid->cursor.point.row; if (row == term->scroll_region.end - 1) term_scroll(term, 1); else { const int new_row = min(row + 1, term->rows - 1); term->grid->cursor.point.row = new_row; term->grid->cur_row = grid_row(term->grid, new_row); } term->grid->cursor.point.col = 0; } static inline void print_insert(struct terminal *term, int width) { if (likely(!term->insert_mode)) return; xassert(width > 0); struct row *row = term->grid->cur_row; const size_t move_count = max(0, term->cols - term->grid->cursor.point.col - width); memmove( &row->cells[term->grid->cursor.point.col + width], &row->cells[term->grid->cursor.point.col], move_count * sizeof(struct cell)); /* Mark moved cells as dirty */ for (size_t i = term->grid->cursor.point.col + width; i < term->cols; i++) row->cells[i].attrs.clean = 0; } static void print_spacer(struct terminal *term, int col, int remaining) { struct grid *grid = term->grid; struct row *row = grid->cur_row; struct cell *cell = &row->cells[col]; cell->wc = CELL_SPACER + remaining; cell->attrs = term->vt.attrs; } /* * Puts a character on the grid. Coordinates are in screen coordinates * (i.e. ‘cursor’ coordinates). * * Does NOT: * - update the cursor * - linewrap * - erase sixels * * Limitations: * - double width characters not supported */ void term_fill(struct terminal *term, int r, int c, uint8_t data, size_t count, bool use_sgr_attrs) { struct row *row = grid_row(term->grid, r); row->dirty = true; xassert(c + count <= term->cols); struct attributes attrs = use_sgr_attrs ? term->vt.attrs : (struct attributes){0}; const struct cell *last = &row->cells[c + count]; for (struct cell *cell = &row->cells[c]; cell < last; cell++) { cell->wc = data; cell->attrs = attrs; /* TODO: why do we print the URI here, and then erase it below? */ if (unlikely(use_sgr_attrs && term->vt.osc8.uri != NULL)) { grid_row_uri_range_put(row, c, term->vt.osc8.uri, term->vt.osc8.id); switch (term->conf->url.osc8_underline) { case OSC8_UNDERLINE_ALWAYS: cell->attrs.url = true; break; case OSC8_UNDERLINE_URL_MODE: break; } } if (unlikely(use_sgr_attrs && (term->vt.underline.style > UNDERLINE_SINGLE || term->vt.underline.color_src != COLOR_DEFAULT))) { grid_row_underline_range_put(row, c, term->vt.underline); } } if (unlikely(row->extra != NULL)) { if (likely(term->vt.osc8.uri != NULL)) grid_row_uri_range_erase(row, c, c + count - 1); if (likely(term->vt.underline.style <= UNDERLINE_SINGLE && term->vt.underline.color_src == COLOR_DEFAULT)) { /* No extended/styled underlines active, so erase any such attributes at the target columns */ grid_row_underline_range_erase(row, c, c + count - 1); } } } void term_print(struct terminal *term, char32_t wc, int width) { xassert(width > 0); struct grid *grid = term->grid; if (unlikely(term->charsets.set[term->charsets.selected] == CHARSET_GRAPHIC) && wc >= 0x60 && wc <= 0x7e) { /* 0x60 - 0x7e */ static const char32_t vt100_0[] = { U'◆', U'▒', U'␉', U'␌', U'␍', U'␊', U'°', U'±', /* ` - g */ U'␤', U'␋', U'┘', U'┐', U'┌', U'└', U'┼', U'⎺', /* h - o */ U'⎻', U'─', U'⎼', U'⎽', U'├', U'┤', U'┴', U'┬', /* p - w */ U'│', U'≤', U'≥', U'π', U'≠', U'£', U'·', /* x - ~ */ }; xassert(width == 1); wc = vt100_0[wc - 0x60]; } print_linewrap(term); print_insert(term, width); int col = grid->cursor.point.col; if (unlikely(width > 1) && likely(term->auto_margin) && col + width > term->cols) { /* Multi-column character that doesn't fit on current line - * pad with spacers */ for (size_t i = col; i < term->cols; i++) print_spacer(term, i, 0); /* And force a line-wrap */ grid->cursor.lcf = 1; print_linewrap(term); col = 0; } sixel_overwrite_at_cursor(term, width); /* *Must* get current cell *after* linewrap+insert */ struct row *row = grid->cur_row; row->dirty = true; row->linebreak = true; struct cell *cell = &row->cells[col]; cell->wc = term->vt.last_printed = wc; cell->attrs = term->vt.attrs; if (term->vt.osc8.uri != NULL) { grid_row_uri_range_put( row, col, term->vt.osc8.uri, term->vt.osc8.id); switch (term->conf->url.osc8_underline) { case OSC8_UNDERLINE_ALWAYS: cell->attrs.url = true; break; case OSC8_UNDERLINE_URL_MODE: break; } } else if (row->extra != NULL) grid_row_uri_range_erase(row, col, col + width - 1); if (unlikely(term->vt.underline.style > UNDERLINE_SINGLE || term->vt.underline.color_src != COLOR_DEFAULT)) { grid_row_underline_range_put(row, col, term->vt.underline); } else if (row->extra != NULL) grid_row_underline_range_erase(row, col, col + width - 1); /* Advance cursor the 'additional' columns while dirty:ing the cells */ for (int i = 1; i < width && (col + 1) < term->cols; i++) { col++; print_spacer(term, col, width - i); } xassert(col < term->cols); /* Advance cursor */ if (unlikely(++col >= term->cols)) { grid->cursor.lcf = true; col--; } else xassert(!grid->cursor.lcf); grid->cursor.point.col = col; } static void ascii_printer_generic(struct terminal *term, char32_t wc) { term_print(term, wc, 1); } static void ascii_printer_fast(struct terminal *term, char32_t wc) { struct grid *grid = term->grid; xassert(term->charsets.set[term->charsets.selected] == CHARSET_ASCII); xassert(!term->insert_mode); xassert(tll_length(grid->sixel_images) == 0); print_linewrap(term); /* *Must* get current cell *after* linewrap+insert */ int col = grid->cursor.point.col; const int uri_start = col; struct row *row = grid->cur_row; row->dirty = true; row->linebreak = true; struct cell *cell = &row->cells[col]; cell->wc = term->vt.last_printed = wc; cell->attrs = term->vt.attrs; /* Advance cursor */ if (unlikely(++col >= term->cols)) { xassert(col == term->cols); grid->cursor.lcf = true; col--; } else xassert(!grid->cursor.lcf); grid->cursor.point.col = col; if (unlikely(row->extra != NULL)) { grid_row_uri_range_erase(row, uri_start, uri_start); grid_row_underline_range_erase(row, uri_start, uri_start); } } static void ascii_printer_single_shift(struct terminal *term, char32_t wc) { ascii_printer_generic(term, wc); term->charsets.selected = term->charsets.saved; term->bits_affecting_ascii_printer.charset = term->charsets.set[term->charsets.selected] != CHARSET_ASCII; term_update_ascii_printer(term); } void term_update_ascii_printer(struct terminal *term) { _Static_assert(sizeof(term->bits_affecting_ascii_printer) == sizeof(uint8_t), "bad size"); void (*new_printer)(struct terminal *term, char32_t wc) = unlikely(term->bits_affecting_ascii_printer.value != 0) ? &ascii_printer_generic : &ascii_printer_fast; #if defined(_DEBUG) && LOG_ENABLE_DBG if (term->ascii_printer != new_printer) { LOG_DBG("switching ASCII printer %s -> %s", term->ascii_printer == &ascii_printer_fast ? "fast" : "generic", new_printer == &ascii_printer_fast ? "fast" : "generic"); } #endif term->ascii_printer = new_printer; } void term_single_shift(struct terminal *term, enum charset_designator idx) { term->charsets.saved = term->charsets.selected; term->charsets.selected = idx; term->ascii_printer = &ascii_printer_single_shift; } enum term_surface term_surface_kind(const struct terminal *term, const struct wl_surface *surface) { if (likely(surface == term->window->surface.surf)) return TERM_SURF_GRID; else if (surface == term->window->csd.surface[CSD_SURF_TITLE].surface.surf) return TERM_SURF_TITLE; else if (surface == term->window->csd.surface[CSD_SURF_LEFT].surface.surf) return TERM_SURF_BORDER_LEFT; else if (surface == term->window->csd.surface[CSD_SURF_RIGHT].surface.surf) return TERM_SURF_BORDER_RIGHT; else if (surface == term->window->csd.surface[CSD_SURF_TOP].surface.surf) return TERM_SURF_BORDER_TOP; else if (surface == term->window->csd.surface[CSD_SURF_BOTTOM].surface.surf) return TERM_SURF_BORDER_BOTTOM; else if (surface == term->window->csd.surface[CSD_SURF_MINIMIZE].surface.surf) return TERM_SURF_BUTTON_MINIMIZE; else if (surface == term->window->csd.surface[CSD_SURF_MAXIMIZE].surface.surf) return TERM_SURF_BUTTON_MAXIMIZE; else if (surface == term->window->csd.surface[CSD_SURF_CLOSE].surface.surf) return TERM_SURF_BUTTON_CLOSE; else return TERM_SURF_NONE; } static bool rows_to_text(const struct terminal *term, int start, int end, int col_start, int col_end, char **text, size_t *len) { struct extraction_context *ctx = extract_begin(SELECTION_NONE, true); if (ctx == NULL) return false; const int grid_rows = term->grid->num_rows; int r = start; while (true) { const struct row *row = term->grid->rows[r]; xassert(row != NULL); const int c_end = r == end ? col_end : term->cols; for (int c = col_start; c < c_end; c++) { if (!extract_one(term, row, &row->cells[c], c, ctx)) goto out; } if (r == end) break; r++; r &= grid_rows - 1; col_start = 0; } out: return extract_finish(ctx, text, len); } bool term_scrollback_to_text(const struct terminal *term, char **text, size_t *len) { const int grid_rows = term->grid->num_rows; int start = (term->grid->offset + term->rows) & (grid_rows - 1); int end = (term->grid->offset + term->rows - 1) & (grid_rows - 1); xassert(start >= 0); xassert(start < grid_rows); xassert(end >= 0); xassert(end < grid_rows); /* If scrollback isn't full yet, this may be NULL, so scan forward * until we find the first non-NULL row */ while (term->grid->rows[start] == NULL) { start++; start &= grid_rows - 1; } while (term->grid->rows[end] == NULL) { end--; if (end < 0) end += term->grid->num_rows; } return rows_to_text(term, start, end, 0, term->cols, text, len); } bool term_view_to_text(const struct terminal *term, char **text, size_t *len) { int start = grid_row_absolute_in_view(term->grid, 0); int end = grid_row_absolute_in_view(term->grid, term->rows - 1); return rows_to_text(term, start, end, 0, term->cols, text, len); } bool term_command_output_to_text(const struct terminal *term, char **text, size_t *len) { int start_row = -1; int end_row = -1; int start_col = -1; int end_col = -1; const struct grid *grid = term->grid; const int sb_end = grid_row_absolute(grid, term->rows - 1); const int sb_start = (sb_end + 1) & (grid->num_rows - 1); int r = sb_end; while (start_row < 0) { const struct row *row = grid->rows[r]; if (row == NULL) break; if (row->shell_integration.cmd_end >= 0) { end_row = r; end_col = row->shell_integration.cmd_end; } if (end_row >= 0 && row->shell_integration.cmd_start >= 0) { start_row = r; start_col = row->shell_integration.cmd_start; } if (r == sb_start) break; r = (r - 1 + grid->num_rows) & (grid->num_rows - 1); } if (start_row < 0) return false; bool ret = rows_to_text(term, start_row, end_row, start_col, end_col, text, len); if (!ret) return false; /* * If the FTCS_COMMAND_FINISHED marker was emitted at the *first* * column, then the *entire* previous line is part of the command * output. *Including* the newline, if any. * * Since rows_to_text() doesn't extract the column * FTCS_COMMAND_FINISHED was emitted at (that would be wrong - * FTCS_COMMAND_FINISHED is emitted *after* the command output, * not at its last character), the extraction logic will not see * the last newline (this is true for all non-line-wise selection * types), and the extracted text will *not* end with a newline. * * Here we try to compensate for that. Note that if 'end_col' is * not 0, then the command output only covers a partial row, and * thus we do *not* want to append a newline. */ if (end_col > 0) { /* Command output covers partial row - don't append newline */ return true; } int next_to_last_row = (end_row - 1 + grid->num_rows) & (grid->num_rows - 1); const struct row *row = grid->rows[next_to_last_row]; /* Add newline if last row has a hard linebreak */ if (row->linebreak) { char *new_text = xrealloc(*text, *len + 1 + 1); if (new_text == NULL) { /* Ignore failure - use text as is (without inserting newline) */ return true; } *text = new_text; (*len)++; (*text)[*len - 1] = '\n'; (*text)[*len] = '\0'; } return true; } bool term_ime_is_enabled(const struct terminal *term) { #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED return term->ime_enabled; #else return false; #endif } void term_ime_enable(struct terminal *term) { #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED if (term->ime_enabled) return; LOG_DBG("IME enabled"); term->ime_enabled = true; /* IME is per seat - enable on all seat currently focusing us */ tll_foreach(term->wl->seats, it) { if (it->item.kbd_focus == term) ime_enable(&it->item); } #endif } void term_ime_disable(struct terminal *term) { #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED if (!term->ime_enabled) return; LOG_DBG("IME disabled"); term->ime_enabled = false; /* IME is per seat - disable on all seat currently focusing us */ tll_foreach(term->wl->seats, it) { if (it->item.kbd_focus == term) ime_disable(&it->item); } #endif } bool term_ime_reset(struct terminal *term) { bool at_least_one_seat_was_reset = false; #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED tll_foreach(term->wl->seats, it) { struct seat *seat = &it->item; if (seat->kbd_focus != term) continue; ime_reset_preedit(seat); at_least_one_seat_was_reset = true; } #endif return at_least_one_seat_was_reset; } void term_ime_set_cursor_rect(struct terminal *term, int x, int y, int width, int height) { #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED tll_foreach(term->wl->seats, it) { if (it->item.kbd_focus == term) { it->item.ime.cursor_rect.pending.x = x; it->item.ime.cursor_rect.pending.y = y; it->item.ime.cursor_rect.pending.width = width; it->item.ime.cursor_rect.pending.height = height; } } #endif } void term_osc8_open(struct terminal *term, uint64_t id, const char *uri) { term_osc8_close(term); xassert(term->vt.osc8.uri == NULL); term->vt.osc8.id = id; term->vt.osc8.uri = xstrdup(uri); term->bits_affecting_ascii_printer.osc8 = true; term_update_ascii_printer(term); } void term_osc8_close(struct terminal *term) { free(term->vt.osc8.uri); term->vt.osc8.uri = NULL; term->vt.osc8.id = 0; term->bits_affecting_ascii_printer.osc8 = false; term_update_ascii_printer(term); } void term_set_user_mouse_cursor(struct terminal *term, const char *cursor) { free(term->mouse_user_cursor); term->mouse_user_cursor = cursor != NULL && strlen(cursor) > 0 ? xstrdup(cursor) : NULL; term_xcursor_update(term); } void term_enable_size_notifications(struct terminal *term) { /* Note: always send current size upon activation, regardless of previous state */ term->size_notifications = true; term_send_size_notification(term); } void term_disable_size_notifications(struct terminal *term) { if (!term->size_notifications) return; term->size_notifications = false; } void term_send_size_notification(struct terminal *term) { if (!term->size_notifications) return; const int height = term->height - term->margins.top - term->margins.bottom; const int width = term->width - term->margins.left - term->margins.right; char buf[128]; const int n = xsnprintf( buf, sizeof(buf), "\033[48;%d;%d;%d;%dt", term->rows, term->cols, height, width); term_to_slave(term, buf, n); }