mirror of
https://codeberg.org/dnkl/foot.git
synced 2026-02-05 04:06:08 -05:00
We do however need access to it, so provide a pointer. The difference is that now we can have a *single* wayland instance, but multiple terminal instances.
1230 lines
38 KiB
C
1230 lines
38 KiB
C
#include <stdlib.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <ctype.h>
|
|
#include <stdbool.h>
|
|
#include <unistd.h>
|
|
#include <assert.h>
|
|
#include <fcntl.h>
|
|
#include <locale.h>
|
|
#include <getopt.h>
|
|
#include <poll.h>
|
|
#include <errno.h>
|
|
|
|
#include <sys/timerfd.h>
|
|
#include <sys/sysinfo.h>
|
|
#include <sys/prctl.h>
|
|
#include <sys/wait.h>
|
|
#include <sys/time.h>
|
|
#include <sys/epoll.h>
|
|
|
|
#include <wayland-client.h>
|
|
#include <wayland-cursor.h>
|
|
#include <xdg-shell.h>
|
|
|
|
#include <xdg-output-unstable-v1.h>
|
|
#include <xdg-decoration-unstable-v1.h>
|
|
|
|
#define LOG_MODULE "main"
|
|
#define LOG_ENABLE_DBG 1
|
|
#include "log.h"
|
|
|
|
#include "config.h"
|
|
#include "fdm.h"
|
|
#include "font.h"
|
|
#include "grid.h"
|
|
#include "input.h"
|
|
#include "render.h"
|
|
#include "selection.h"
|
|
#include "shm.h"
|
|
#include "slave.h"
|
|
#include "terminal.h"
|
|
#include "tokenize.h"
|
|
#include "version.h"
|
|
#include "vt.h"
|
|
|
|
#define min(x, y) ((x) < (y) ? (x) : (y))
|
|
#define max(x, y) ((x) > (y) ? (x) : (y))
|
|
|
|
static void
|
|
shm_format(void *data, struct wl_shm *wl_shm, uint32_t format)
|
|
{
|
|
struct wayland *wayl = data;
|
|
if (format == WL_SHM_FORMAT_ARGB8888)
|
|
wayl->have_argb8888 = true;
|
|
}
|
|
|
|
static const struct wl_shm_listener shm_listener = {
|
|
.format = &shm_format,
|
|
};
|
|
|
|
static void
|
|
xdg_wm_base_ping(void *data, struct xdg_wm_base *shell, uint32_t serial)
|
|
{
|
|
LOG_DBG("wm base ping");
|
|
xdg_wm_base_pong(shell, serial);
|
|
}
|
|
|
|
static const struct xdg_wm_base_listener xdg_wm_base_listener = {
|
|
.ping = &xdg_wm_base_ping,
|
|
};
|
|
|
|
static void
|
|
seat_handle_capabilities(void *data, struct wl_seat *wl_seat,
|
|
enum wl_seat_capability caps)
|
|
{
|
|
struct wayland *wayl = data;
|
|
|
|
if (wayl->keyboard != NULL) {
|
|
wl_keyboard_release(wayl->keyboard);
|
|
wayl->keyboard = NULL;
|
|
}
|
|
|
|
if (wayl->pointer.pointer != NULL) {
|
|
wl_pointer_release(wayl->pointer.pointer);
|
|
wayl->pointer.pointer = NULL;
|
|
}
|
|
|
|
if (caps & WL_SEAT_CAPABILITY_KEYBOARD) {
|
|
wayl->keyboard = wl_seat_get_keyboard(wl_seat);
|
|
wl_keyboard_add_listener(wayl->keyboard, &keyboard_listener, wayl);
|
|
}
|
|
|
|
if (caps & WL_SEAT_CAPABILITY_POINTER) {
|
|
wayl->pointer.pointer = wl_seat_get_pointer(wl_seat);
|
|
wl_pointer_add_listener(wayl->pointer.pointer, &pointer_listener, wayl);
|
|
}
|
|
}
|
|
|
|
static void
|
|
seat_handle_name(void *data, struct wl_seat *wl_seat, const char *name)
|
|
{
|
|
}
|
|
|
|
static const struct wl_seat_listener seat_listener = {
|
|
.capabilities = seat_handle_capabilities,
|
|
.name = seat_handle_name,
|
|
};
|
|
|
|
static void
|
|
output_geometry(void *data, struct wl_output *wl_output, int32_t x, int32_t y,
|
|
int32_t physical_width, int32_t physical_height,
|
|
int32_t subpixel, const char *make, const char *model,
|
|
int32_t transform)
|
|
{
|
|
struct monitor *mon = data;
|
|
mon->width_mm = physical_width;
|
|
mon->height_mm = physical_height;
|
|
}
|
|
|
|
static void
|
|
output_mode(void *data, struct wl_output *wl_output, uint32_t flags,
|
|
int32_t width, int32_t height, int32_t refresh)
|
|
{
|
|
if ((flags & WL_OUTPUT_MODE_CURRENT) == 0)
|
|
return;
|
|
|
|
struct monitor *mon = data;
|
|
mon->refresh = (float)refresh / 1000;
|
|
}
|
|
|
|
static void
|
|
output_done(void *data, struct wl_output *wl_output)
|
|
{
|
|
}
|
|
|
|
static void
|
|
output_scale(void *data, struct wl_output *wl_output, int32_t factor)
|
|
{
|
|
struct monitor *mon = data;
|
|
struct terminal *term = mon->wayl->term;
|
|
|
|
mon->scale = factor;
|
|
|
|
render_resize(term, term->width / term->scale, term->height / term->scale);
|
|
render_reload_cursor_theme(term);
|
|
}
|
|
|
|
static const struct wl_output_listener output_listener = {
|
|
.geometry = &output_geometry,
|
|
.mode = &output_mode,
|
|
.done = &output_done,
|
|
.scale = &output_scale,
|
|
};
|
|
|
|
static void
|
|
xdg_output_handle_logical_position(
|
|
void *data, struct zxdg_output_v1 *xdg_output, int32_t x, int32_t y)
|
|
{
|
|
struct monitor *mon = data;
|
|
mon->x = x;
|
|
mon->y = y;
|
|
}
|
|
|
|
static void
|
|
xdg_output_handle_logical_size(void *data, struct zxdg_output_v1 *xdg_output,
|
|
int32_t width, int32_t height)
|
|
{
|
|
struct monitor *mon = data;
|
|
mon->width_px = width;
|
|
mon->height_px = height;
|
|
}
|
|
|
|
static void
|
|
xdg_output_handle_done(void *data, struct zxdg_output_v1 *xdg_output)
|
|
{
|
|
}
|
|
|
|
static void
|
|
xdg_output_handle_name(void *data, struct zxdg_output_v1 *xdg_output,
|
|
const char *name)
|
|
{
|
|
struct monitor *mon = data;
|
|
mon->name = strdup(name);
|
|
}
|
|
|
|
static void
|
|
xdg_output_handle_description(void *data, struct zxdg_output_v1 *xdg_output,
|
|
const char *description)
|
|
{
|
|
}
|
|
|
|
static struct zxdg_output_v1_listener xdg_output_listener = {
|
|
.logical_position = xdg_output_handle_logical_position,
|
|
.logical_size = xdg_output_handle_logical_size,
|
|
.done = xdg_output_handle_done,
|
|
.name = xdg_output_handle_name,
|
|
.description = xdg_output_handle_description,
|
|
};
|
|
|
|
static void
|
|
handle_global(void *data, struct wl_registry *registry,
|
|
uint32_t name, const char *interface, uint32_t version)
|
|
{
|
|
LOG_DBG("global: %s, version=%u", interface, version);
|
|
struct wayland *wayl = data;
|
|
|
|
if (strcmp(interface, wl_compositor_interface.name) == 0) {
|
|
wayl->compositor = wl_registry_bind(
|
|
wayl->registry, name, &wl_compositor_interface, 4);
|
|
}
|
|
|
|
else if (strcmp(interface, wl_subcompositor_interface.name) == 0) {
|
|
wayl->sub_compositor = wl_registry_bind(
|
|
wayl->registry, name, &wl_subcompositor_interface, 1);
|
|
}
|
|
|
|
else if (strcmp(interface, wl_shm_interface.name) == 0) {
|
|
wayl->shm = wl_registry_bind(
|
|
wayl->registry, name, &wl_shm_interface, 1);
|
|
wl_shm_add_listener(wayl->shm, &shm_listener, wayl);
|
|
wl_display_roundtrip(wayl->display);
|
|
}
|
|
|
|
else if (strcmp(interface, xdg_wm_base_interface.name) == 0) {
|
|
wayl->shell = wl_registry_bind(
|
|
wayl->registry, name, &xdg_wm_base_interface, 1);
|
|
xdg_wm_base_add_listener(wayl->shell, &xdg_wm_base_listener, wayl);
|
|
}
|
|
|
|
else if (strcmp(interface, zxdg_decoration_manager_v1_interface.name) == 0)
|
|
wayl->xdg_decoration_manager = wl_registry_bind(
|
|
wayl->registry, name, &zxdg_decoration_manager_v1_interface, 1);
|
|
|
|
else if (strcmp(interface, wl_seat_interface.name) == 0) {
|
|
wayl->seat = wl_registry_bind(
|
|
wayl->registry, name, &wl_seat_interface, 5);
|
|
wl_seat_add_listener(wayl->seat, &seat_listener, wayl);
|
|
wl_display_roundtrip(wayl->display);
|
|
}
|
|
|
|
else if (strcmp(interface, zxdg_output_manager_v1_interface.name) == 0) {
|
|
wayl->xdg_output_manager = wl_registry_bind(
|
|
wayl->registry, name, &zxdg_output_manager_v1_interface, min(version, 2));
|
|
}
|
|
|
|
else if (strcmp(interface, wl_output_interface.name) == 0) {
|
|
struct wl_output *output = wl_registry_bind(
|
|
wayl->registry, name, &wl_output_interface, 3);
|
|
|
|
tll_push_back(
|
|
wayl->monitors, ((struct monitor){.wayl = wayl, .output = output}));
|
|
|
|
struct monitor *mon = &tll_back(wayl->monitors);
|
|
wl_output_add_listener(output, &output_listener, mon);
|
|
|
|
mon->xdg = zxdg_output_manager_v1_get_xdg_output(
|
|
wayl->xdg_output_manager, mon->output);
|
|
zxdg_output_v1_add_listener(mon->xdg, &xdg_output_listener, mon);
|
|
wl_display_roundtrip(wayl->display);
|
|
}
|
|
|
|
else if (strcmp(interface, wl_data_device_manager_interface.name) == 0) {
|
|
wayl->data_device_manager = wl_registry_bind(
|
|
wayl->registry, name, &wl_data_device_manager_interface, 1);
|
|
}
|
|
|
|
else if (strcmp(interface, zwp_primary_selection_device_manager_v1_interface.name) == 0) {
|
|
wayl->primary_selection_device_manager = wl_registry_bind(
|
|
wayl->registry, name, &zwp_primary_selection_device_manager_v1_interface, 1);
|
|
}
|
|
}
|
|
|
|
static void
|
|
surface_enter(void *data, struct wl_surface *wl_surface,
|
|
struct wl_output *wl_output)
|
|
{
|
|
struct wayland *wayl = data;
|
|
struct terminal *term = wayl_terminal_from_surface(wayl, wl_surface);
|
|
|
|
tll_foreach(wayl->monitors, it) {
|
|
if (it->item.output == wl_output) {
|
|
LOG_DBG("mapped on %s", it->item.name);
|
|
tll_push_back(term->window.on_outputs, &it->item);
|
|
|
|
/* Resize, since scale-to-use may have changed */
|
|
render_resize(term, term->width / term->scale, term->height / term->scale);
|
|
render_reload_cursor_theme(term);
|
|
return;
|
|
}
|
|
}
|
|
|
|
LOG_ERR("mapped on unknown output");
|
|
}
|
|
|
|
static void
|
|
surface_leave(void *data, struct wl_surface *wl_surface,
|
|
struct wl_output *wl_output)
|
|
{
|
|
struct wayland *wayl = data;
|
|
struct terminal *term = wayl_terminal_from_surface(wayl, wl_surface);
|
|
|
|
tll_foreach(term->window.on_outputs, it) {
|
|
if (it->item->output != wl_output)
|
|
continue;
|
|
|
|
LOG_DBG("unmapped from %s", it->item->name);
|
|
tll_remove(term->window.on_outputs, it);
|
|
|
|
/* Resize, since scale-to-use may have changed */
|
|
render_resize(term, term->width / term->scale, term->height / term->scale);
|
|
render_reload_cursor_theme(term);
|
|
return;
|
|
}
|
|
|
|
LOG_ERR("unmapped from unknown output");
|
|
}
|
|
|
|
static const struct wl_surface_listener surface_listener = {
|
|
.enter = &surface_enter,
|
|
.leave = &surface_leave,
|
|
};
|
|
|
|
static void
|
|
xdg_toplevel_configure(void *data, struct xdg_toplevel *xdg_toplevel,
|
|
int32_t width, int32_t height, struct wl_array *states)
|
|
{
|
|
LOG_DBG("xdg-toplevel: configure: %dx%d", width, height);
|
|
|
|
if (width <= 0 || height <= 0)
|
|
return;
|
|
|
|
struct wayland *wayl = data;
|
|
struct terminal *term = wayl_terminal_from_xdg_toplevel(wayl, xdg_toplevel);
|
|
render_resize(term, width, height);
|
|
}
|
|
|
|
static void
|
|
xdg_toplevel_close(void *data, struct xdg_toplevel *xdg_toplevel)
|
|
{
|
|
struct wayland *wayl = data;
|
|
struct terminal *term = wayl_terminal_from_xdg_toplevel(wayl, xdg_toplevel);
|
|
LOG_DBG("xdg-toplevel: close");
|
|
|
|
term->quit = true;
|
|
wl_display_roundtrip(wayl->display);
|
|
}
|
|
|
|
static const struct xdg_toplevel_listener xdg_toplevel_listener = {
|
|
.configure = &xdg_toplevel_configure,
|
|
.close = &xdg_toplevel_close,
|
|
};
|
|
|
|
static void
|
|
xdg_surface_configure(void *data, struct xdg_surface *xdg_surface,
|
|
uint32_t serial)
|
|
{
|
|
//LOG_DBG("xdg-surface: configure");
|
|
xdg_surface_ack_configure(xdg_surface, serial);
|
|
}
|
|
|
|
static const struct xdg_surface_listener xdg_surface_listener = {
|
|
.configure = &xdg_surface_configure,
|
|
};
|
|
|
|
static void
|
|
xdg_toplevel_decoration_configure(void *data,
|
|
struct zxdg_toplevel_decoration_v1 *zxdg_toplevel_decoration_v1,
|
|
uint32_t mode)
|
|
{
|
|
switch (mode) {
|
|
case ZXDG_TOPLEVEL_DECORATION_V1_MODE_CLIENT_SIDE:
|
|
LOG_ERR("unimplemented: client-side decorations");
|
|
break;
|
|
|
|
case ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE:
|
|
LOG_DBG("using server-side decorations");
|
|
break;
|
|
|
|
default:
|
|
LOG_ERR("unimplemented: unknown XDG toplevel decoration mode: %u", mode);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static const struct zxdg_toplevel_decoration_v1_listener xdg_toplevel_decoration_listener = {
|
|
.configure = &xdg_toplevel_decoration_configure,
|
|
};
|
|
|
|
static void
|
|
handle_global_remove(void *data, struct wl_registry *registry, uint32_t name)
|
|
{
|
|
LOG_WARN("global removed: %u", name);
|
|
assert(false);
|
|
}
|
|
|
|
static const struct wl_registry_listener registry_listener = {
|
|
.global = &handle_global,
|
|
.global_remove = &handle_global_remove,
|
|
};
|
|
|
|
static bool
|
|
fdm_wayl(struct fdm *fdm, int fd, int events, void *data)
|
|
{
|
|
struct wayland *wayl = data;
|
|
int event_count = wl_display_dispatch(wayl->display);
|
|
|
|
if (events & EPOLLHUP) {
|
|
LOG_ERR("disconnected from Wayland");
|
|
return false;
|
|
}
|
|
|
|
return event_count != -1 && !wayl->term->quit;
|
|
}
|
|
|
|
static bool
|
|
fdm_ptmx(struct fdm *fdm, int fd, int events, void *data)
|
|
{
|
|
struct terminal *term = data;
|
|
|
|
if (events & EPOLLHUP) {
|
|
term->quit = true;
|
|
|
|
if (!(events & EPOLLIN))
|
|
return false;
|
|
}
|
|
|
|
assert(events & EPOLLIN);
|
|
|
|
uint8_t buf[24 * 1024];
|
|
ssize_t count = read(term->ptmx, buf, sizeof(buf));
|
|
|
|
if (count < 0) {
|
|
if (errno == EAGAIN)
|
|
return true;
|
|
|
|
LOG_ERRNO("failed to read from pseudo terminal");
|
|
return false;
|
|
}
|
|
|
|
vt_from_slave(term, buf, count);
|
|
|
|
/*
|
|
* We likely need to re-render. But, we don't want to
|
|
* do it immediately. Often, a single client operation
|
|
* is done through multiple writes. Many times, we're
|
|
* so fast that we render mid-operation frames.
|
|
*
|
|
* For example, we might end up rendering a frame
|
|
* where the client just erased a line, while in the
|
|
* next frame, the client wrote to the same line. This
|
|
* causes screen "flashes".
|
|
*
|
|
* Mitigate by always incuring a small delay before
|
|
* rendering the next frame. This gives the client
|
|
* some time to finish the operation (and thus gives
|
|
* us time to receive the last writes before doing any
|
|
* actual rendering).
|
|
*
|
|
* We incur this delay *every* time we receive
|
|
* input. To ensure we don't delay rendering
|
|
* indefinitely, we start a second timer that is only
|
|
* reset when we render.
|
|
*
|
|
* Note that when the client is producing data at a
|
|
* very high pace, we're rate limited by the wayland
|
|
* compositor anyway. The delay we introduce here only
|
|
* has any effect when the renderer is idle.
|
|
*
|
|
* TODO: this adds input latency. Can we somehow hint
|
|
* ourselves we just received keyboard input, and in
|
|
* this case *not* delay rendering?
|
|
*/
|
|
if (term->window.frame_callback == NULL) {
|
|
/* First timeout - reset each time we receive input. */
|
|
timerfd_settime(
|
|
term->delayed_render_timer.lower_fd, 0,
|
|
&(struct itimerspec){.it_value = {.tv_nsec = 1000000}},
|
|
NULL);
|
|
|
|
/* Second timeout - only reset when we render. Set to one
|
|
* frame (assuming 60Hz) */
|
|
if (!term->delayed_render_timer.is_armed) {
|
|
timerfd_settime(
|
|
term->delayed_render_timer.upper_fd, 0,
|
|
&(struct itimerspec){.it_value = {.tv_nsec = 16666666}},
|
|
NULL);
|
|
term->delayed_render_timer.is_armed = true;
|
|
}
|
|
}
|
|
|
|
return !(events & EPOLLHUP);
|
|
}
|
|
|
|
static bool
|
|
fdm_repeat(struct fdm *fdm, int fd, int events, void *data)
|
|
{
|
|
if (events & EPOLLHUP)
|
|
return false;
|
|
|
|
struct wayland *wayl = data;
|
|
uint64_t expiration_count;
|
|
ssize_t ret = read(
|
|
wayl->kbd.repeat.fd, &expiration_count, sizeof(expiration_count));
|
|
|
|
if (ret < 0) {
|
|
if (errno == EAGAIN)
|
|
return true;
|
|
|
|
LOG_ERRNO("failed to read repeat key from repeat timer fd");
|
|
return false;
|
|
}
|
|
|
|
wayl->kbd.repeat.dont_re_repeat = true;
|
|
for (size_t i = 0; i < expiration_count; i++)
|
|
input_repeat(wayl, wayl->kbd.repeat.key);
|
|
wayl->kbd.repeat.dont_re_repeat = false;
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
fdm_flash(struct fdm *fdm, int fd, int events, void *data)
|
|
{
|
|
if (events & EPOLLHUP)
|
|
return false;
|
|
|
|
struct terminal *term = data;
|
|
uint64_t expiration_count;
|
|
ssize_t ret = read(
|
|
term->flash.fd, &expiration_count, sizeof(expiration_count));
|
|
|
|
if (ret < 0) {
|
|
if (errno == EAGAIN)
|
|
return true;
|
|
|
|
LOG_ERRNO("failed to read flash timer");
|
|
return false;
|
|
}
|
|
|
|
LOG_DBG("flash timer expired %llu times",
|
|
(unsigned long long)expiration_count);
|
|
|
|
term->flash.active = false;
|
|
term_damage_view(term);
|
|
render_refresh(term);
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
fdm_blink(struct fdm *fdm, int fd, int events, void *data)
|
|
{
|
|
if (events & EPOLLHUP)
|
|
return false;
|
|
|
|
struct terminal *term = data;
|
|
uint64_t expiration_count;
|
|
ssize_t ret = read(
|
|
term->blink.fd, &expiration_count, sizeof(expiration_count));
|
|
|
|
if (ret < 0) {
|
|
if (errno == EAGAIN)
|
|
return true;
|
|
|
|
LOG_ERRNO("failed to read blink timer");
|
|
return false;
|
|
}
|
|
|
|
LOG_DBG("blink timer expired %llu times",
|
|
(unsigned long long)expiration_count);
|
|
|
|
term->blink.state = term->blink.state == BLINK_ON
|
|
? BLINK_OFF : BLINK_ON;
|
|
|
|
/* Scan all visible cells and mark rows with blinking cells dirty */
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
render_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;
|
|
assert(term->delayed_render_timer.is_armed);
|
|
|
|
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) && errno != EAGAIN)
|
|
LOG_ERRNO("failed to read timeout timer");
|
|
else if (ret1 > 0 || ret2 > 0) {
|
|
render_refresh(term);
|
|
|
|
/* Reset timers */
|
|
term->delayed_render_timer.is_armed = false;
|
|
timerfd_settime(term->delayed_render_timer.lower_fd, 0, &(struct itimerspec){.it_value = {0}}, NULL);
|
|
timerfd_settime(term->delayed_render_timer.upper_fd, 0, &(struct itimerspec){.it_value = {0}}, NULL);
|
|
} else
|
|
assert(false);
|
|
|
|
return true;
|
|
}
|
|
|
|
static void
|
|
print_usage(const char *prog_name)
|
|
{
|
|
printf("Usage: %s [OPTION]...\n", prog_name);
|
|
printf("\n");
|
|
printf("Options:\n");
|
|
printf(" -f,--font=FONT comma separated list of fonts in fontconfig format (monospace)\n"
|
|
" -t,--term=TERM value to set the environment variable TERM to (foot)\n"
|
|
" -g,--geometry=WIDTHxHEIGHT set initial width and height\n"
|
|
" -v,--version show the version number and quit\n");
|
|
printf("\n");
|
|
}
|
|
|
|
int
|
|
main(int argc, char *const *argv)
|
|
{
|
|
int ret = EXIT_FAILURE;
|
|
|
|
/* Startup notifications; we don't support it, but must ensure we
|
|
* don't pass this on to programs launched by us */
|
|
unsetenv("DESKTOP_STARTUP_ID");
|
|
|
|
struct config conf = {NULL};
|
|
if (!config_load(&conf))
|
|
return ret;
|
|
|
|
const char *const prog_name = argv[0];
|
|
|
|
static const struct option longopts[] = {
|
|
{"term", required_argument, 0, 't'},
|
|
{"font", required_argument, 0, 'f'},
|
|
{"geometry", required_argument, 0, 'g'},
|
|
{"version", no_argument, 0, 'v'},
|
|
{"help", no_argument, 0, 'h'},
|
|
{NULL, no_argument, 0, 0},
|
|
};
|
|
|
|
while (true) {
|
|
int c = getopt_long(argc, argv, ":t:f:g:vh", longopts, NULL);
|
|
if (c == -1)
|
|
break;
|
|
|
|
switch (c) {
|
|
case 't':
|
|
free(conf.term);
|
|
conf.term = strdup(optarg);
|
|
break;
|
|
|
|
case 'f':
|
|
tll_free_and_free(conf.fonts, free);
|
|
//tll_push_back(conf.fonts, strdup(optarg));
|
|
for (char *font = strtok(optarg, ","); font != NULL; font = strtok(NULL, ",")) {
|
|
|
|
/* Strip leading spaces */
|
|
while (*font != '\0' && isspace(*font))
|
|
font++;
|
|
|
|
/* Strip trailing spaces */
|
|
char *end = font + strlen(font);
|
|
assert(*end == '\0');
|
|
end--;
|
|
while (end > font && isspace(*end))
|
|
*(end--) = '\0';
|
|
|
|
if (strlen(font) == 0)
|
|
continue;
|
|
|
|
tll_push_back(conf.fonts, strdup(font));
|
|
}
|
|
break;
|
|
|
|
case 'g': {
|
|
unsigned width, height;
|
|
if (sscanf(optarg, "%ux%u", &width, &height) != 2 || width == 0 || height == 0) {
|
|
fprintf(stderr, "error: invalid geometry: %s\n", optarg);
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
conf.width = width;
|
|
conf.height = height;
|
|
break;
|
|
}
|
|
|
|
case 'v':
|
|
printf("foot version %s\n", FOOT_VERSION);
|
|
return EXIT_SUCCESS;
|
|
|
|
case 'h':
|
|
print_usage(prog_name);
|
|
return EXIT_SUCCESS;
|
|
|
|
case ':':
|
|
fprintf(stderr, "error: -%c: missing required argument\n", optopt);
|
|
return EXIT_FAILURE;
|
|
|
|
case '?':
|
|
fprintf(stderr, "error: -%c: invalid option\n", optopt);
|
|
return EXIT_FAILURE;
|
|
}
|
|
}
|
|
|
|
argc -= optind;
|
|
argv += optind;
|
|
|
|
setlocale(LC_ALL, "");
|
|
setenv("TERM", conf.term, 1);
|
|
|
|
struct fdm *fdm = NULL;
|
|
|
|
struct wayland wayl = {
|
|
.kbd = {
|
|
.repeat = {
|
|
.fd = timerfd_create(CLOCK_BOOTTIME, TFD_CLOEXEC | TFD_NONBLOCK),
|
|
},
|
|
},
|
|
};
|
|
|
|
struct terminal term = {
|
|
.quit = false,
|
|
.ptmx = posix_openpt(O_RDWR | O_NOCTTY),
|
|
.cursor_keys_mode = CURSOR_KEYS_NORMAL,
|
|
.keypad_keys_mode = KEYPAD_NUMERICAL,
|
|
.auto_margin = true,
|
|
.window_title_stack = tll_init(),
|
|
.scale = 1,
|
|
.flash = {
|
|
.fd = timerfd_create(CLOCK_BOOTTIME, TFD_CLOEXEC | TFD_NONBLOCK),
|
|
},
|
|
.blink = {
|
|
.fd = timerfd_create(CLOCK_BOOTTIME, TFD_CLOEXEC | TFD_NONBLOCK),
|
|
},
|
|
.vt = {
|
|
.state = 1, /* STATE_GROUND */
|
|
.attrs = {
|
|
//.foreground = conf.colors.fg,
|
|
//.background = conf.colors.bg
|
|
},
|
|
},
|
|
.colors = {
|
|
.default_fg = conf.colors.fg,
|
|
.default_bg = conf.colors.bg,
|
|
.default_table = {
|
|
conf.colors.regular[0],
|
|
conf.colors.regular[1],
|
|
conf.colors.regular[2],
|
|
conf.colors.regular[3],
|
|
conf.colors.regular[4],
|
|
conf.colors.regular[5],
|
|
conf.colors.regular[6],
|
|
conf.colors.regular[7],
|
|
|
|
conf.colors.bright[0],
|
|
conf.colors.bright[1],
|
|
conf.colors.bright[2],
|
|
conf.colors.bright[3],
|
|
conf.colors.bright[4],
|
|
conf.colors.bright[5],
|
|
conf.colors.bright[6],
|
|
conf.colors.bright[7],
|
|
},
|
|
.alpha = conf.colors.alpha,
|
|
},
|
|
.default_cursor_style = conf.cursor.style,
|
|
.cursor_style = conf.cursor.style,
|
|
.default_cursor_color = {
|
|
.text = conf.cursor.color.text,
|
|
.cursor = conf.cursor.color.cursor,
|
|
},
|
|
.cursor_color = {
|
|
.text = conf.cursor.color.text,
|
|
.cursor = conf.cursor.color.cursor,
|
|
},
|
|
.selection = {
|
|
.start = {-1, -1},
|
|
.end = {-1, -1},
|
|
},
|
|
.normal = {.damage = tll_init(), .scroll_damage = tll_init()},
|
|
.alt = {.damage = tll_init(), .scroll_damage = tll_init()},
|
|
.grid = &term.normal,
|
|
.wl = &wayl,
|
|
.render = {
|
|
.scrollback_lines = conf.scrollback_lines,
|
|
.workers = {
|
|
.count = conf.render_worker_count,
|
|
.queue = tll_init(),
|
|
},
|
|
},
|
|
.delayed_render_timer = {
|
|
.is_armed = false,
|
|
.lower_fd = timerfd_create(CLOCK_REALTIME, TFD_NONBLOCK | TFD_CLOEXEC),
|
|
.upper_fd = timerfd_create(CLOCK_REALTIME, TFD_NONBLOCK | TFD_CLOEXEC),
|
|
},
|
|
};
|
|
|
|
LOG_INFO("using %zu rendering threads", term.render.workers.count);
|
|
|
|
struct render_worker_context worker_context[term.render.workers.count];
|
|
|
|
/* Initialize 'current' colors from the default colors */
|
|
term.colors.fg = term.colors.default_fg;
|
|
term.colors.bg = term.colors.default_bg;
|
|
|
|
/* Initialize the 256 gray-scale color cube */
|
|
{
|
|
/* First 16 entries have already been initialized from conf */
|
|
for (size_t r = 0; r < 6; r++) {
|
|
for (size_t g = 0; g < 6; g++) {
|
|
for (size_t b = 0; b < 6; b++) {
|
|
term.colors.default_table[16 + r * 6 * 6 + g * 6 + b]
|
|
= r * 51 << 16 | g * 51 << 8 | b * 51;
|
|
}
|
|
}
|
|
}
|
|
|
|
for (size_t i = 0; i < 24; i++)
|
|
term.colors.default_table[232 + i] = i * 11 << 16 | i * 11 << 8 | i * 11;
|
|
|
|
memcpy(term.colors.table, term.colors.default_table, sizeof(term.colors.table));
|
|
}
|
|
|
|
if (term.ptmx == -1) {
|
|
LOG_ERR("failed to open pseudo terminal");
|
|
goto out;
|
|
}
|
|
|
|
if (term.flash.fd == -1 || term.blink.fd == -1 || term.wl->kbd.repeat.fd == -1) {
|
|
LOG_ERR("failed to create timers");
|
|
goto out;
|
|
}
|
|
|
|
sem_init(&term.render.workers.start, 0, 0);
|
|
sem_init(&term.render.workers.done, 0, 0);
|
|
mtx_init(&term.render.workers.lock, mtx_plain);
|
|
cnd_init(&term.render.workers.cond);
|
|
|
|
term.render.workers.threads = calloc(term.render.workers.count, sizeof(term.render.workers.threads[0]));
|
|
for (size_t i = 0; i < term.render.workers.count; i++) {
|
|
worker_context[i].term = &term;
|
|
worker_context[i].my_id = 1 + i;
|
|
thrd_create(&term.render.workers.threads[i], &render_worker_thread, &worker_context[i]);
|
|
}
|
|
|
|
font_list_t font_names = tll_init();
|
|
tll_foreach(conf.fonts, it)
|
|
tll_push_back(font_names, it->item);
|
|
|
|
if ((term.fonts[0] = font_from_name(font_names, "")) == NULL) {
|
|
tll_free(font_names);
|
|
goto out;
|
|
}
|
|
|
|
term.fonts[1] = font_from_name(font_names, "style=bold");
|
|
term.fonts[2] = font_from_name(font_names, "style=italic");
|
|
term.fonts[3] = font_from_name(font_names, "style=bold italic");
|
|
|
|
tll_free(font_names);
|
|
|
|
{
|
|
FT_Face ft_face = term.fonts[0]->face;
|
|
int max_x_advance = ft_face->size->metrics.max_advance / 64;
|
|
int height = ft_face->size->metrics.height / 64;
|
|
int descent = ft_face->size->metrics.descender / 64;
|
|
int ascent = ft_face->size->metrics.ascender / 64;
|
|
|
|
term.fextents.height = height * term.fonts[0]->pixel_size_fixup;
|
|
term.fextents.descent = -descent * term.fonts[0]->pixel_size_fixup;
|
|
term.fextents.ascent = ascent * term.fonts[0]->pixel_size_fixup;
|
|
term.fextents.max_x_advance = max_x_advance * term.fonts[0]->pixel_size_fixup;
|
|
|
|
LOG_DBG("metrics: height: %d, descent: %d, ascent: %d, x-advance: %d",
|
|
height, descent, ascent, max_x_advance);
|
|
}
|
|
|
|
term.cell_width = (int)ceil(term.fextents.max_x_advance);
|
|
term.cell_height = (int)ceil(term.fextents.height);
|
|
LOG_INFO("cell width=%d, height=%d", term.cell_width, term.cell_height);
|
|
|
|
term.wl->term = &term;
|
|
term.wl->display = wl_display_connect(NULL);
|
|
if (term.wl->display == NULL) {
|
|
LOG_ERR("failed to connect to wayland; no compositor running?");
|
|
goto out;
|
|
}
|
|
|
|
term.wl->registry = wl_display_get_registry(term.wl->display);
|
|
if (term.wl->registry == NULL) {
|
|
LOG_ERR("failed to get wayland registry");
|
|
goto out;
|
|
}
|
|
|
|
wl_registry_add_listener(term.wl->registry, ®istry_listener, term.wl);
|
|
wl_display_roundtrip(term.wl->display);
|
|
|
|
if (term.wl->compositor == NULL) {
|
|
LOG_ERR("no compositor");
|
|
goto out;
|
|
}
|
|
if (term.wl->shm == NULL) {
|
|
LOG_ERR("no shared memory buffers interface");
|
|
goto out;
|
|
}
|
|
if (term.wl->shell == NULL) {
|
|
LOG_ERR("no XDG shell interface");
|
|
goto out;
|
|
}
|
|
if (!term.wl->have_argb8888) {
|
|
LOG_ERR("compositor does not support ARGB surfaces");
|
|
goto out;
|
|
}
|
|
if (term.wl->seat == NULL) {
|
|
LOG_ERR("no seat available");
|
|
goto out;
|
|
}
|
|
if (term.wl->data_device_manager == NULL) {
|
|
LOG_ERR("no clipboard available "
|
|
"(wl_data_device_manager not implemented by server)");
|
|
goto out;
|
|
}
|
|
if (term.wl->primary_selection_device_manager == NULL)
|
|
LOG_WARN("no primary selection available");
|
|
|
|
tll_foreach(term.wl->monitors, it) {
|
|
LOG_INFO("%s: %dx%d+%dx%d (scale=%d, refresh=%.2fHz)",
|
|
it->item.name, it->item.width_px, it->item.height_px,
|
|
it->item.x, it->item.y, it->item.scale, it->item.refresh);
|
|
}
|
|
|
|
/* Clipboard */
|
|
term.wl->data_device = wl_data_device_manager_get_data_device(
|
|
term.wl->data_device_manager, term.wl->seat);
|
|
wl_data_device_add_listener(term.wl->data_device, &data_device_listener, term.wl);
|
|
|
|
/* Primary selection */
|
|
if (term.wl->primary_selection_device_manager != NULL) {
|
|
term.wl->primary_selection_device = zwp_primary_selection_device_manager_v1_get_device(
|
|
term.wl->primary_selection_device_manager, term.wl->seat);
|
|
zwp_primary_selection_device_v1_add_listener(
|
|
term.wl->primary_selection_device, &primary_selection_device_listener, term.wl);
|
|
}
|
|
|
|
/* Cursor */
|
|
unsigned cursor_size = 24;
|
|
const char *cursor_theme = getenv("XCURSOR_THEME");
|
|
|
|
{
|
|
const char *env_cursor_size = getenv("XCURSOR_SIZE");
|
|
if (env_cursor_size != NULL) {
|
|
unsigned size;
|
|
if (sscanf(env_cursor_size, "%u", &size) == 1)
|
|
cursor_size = size;
|
|
}
|
|
}
|
|
|
|
/* Note: theme is (re)loaded on scale and output changes */
|
|
LOG_INFO("cursor theme: %s, size: %u", cursor_theme, cursor_size);
|
|
term.wl->pointer.size = cursor_size;
|
|
term.wl->pointer.theme_name = cursor_theme != NULL ? strdup(cursor_theme) : NULL;
|
|
|
|
term.wl->pointer.surface = wl_compositor_create_surface(term.wl->compositor);
|
|
if (term.wl->pointer.surface == NULL) {
|
|
LOG_ERR("failed to create cursor surface");
|
|
goto out;
|
|
}
|
|
|
|
/* Main window */
|
|
term.window.surface = wl_compositor_create_surface(term.wl->compositor);
|
|
if (term.window.surface == NULL) {
|
|
LOG_ERR("failed to create wayland surface");
|
|
goto out;
|
|
}
|
|
|
|
wl_surface_add_listener(term.window.surface, &surface_listener, term.wl);
|
|
|
|
term.window.xdg_surface = xdg_wm_base_get_xdg_surface(term.wl->shell, term.window.surface);
|
|
xdg_surface_add_listener(term.window.xdg_surface, &xdg_surface_listener, term.wl);
|
|
|
|
term.window.xdg_toplevel = xdg_surface_get_toplevel(term.window.xdg_surface);
|
|
xdg_toplevel_add_listener(term.window.xdg_toplevel, &xdg_toplevel_listener, term.wl);
|
|
|
|
xdg_toplevel_set_app_id(term.window.xdg_toplevel, "foot");
|
|
term_set_window_title(&term, "foot");
|
|
|
|
/* Request server-side decorations */
|
|
term.window.xdg_toplevel_decoration = zxdg_decoration_manager_v1_get_toplevel_decoration(
|
|
term.wl->xdg_decoration_manager, term.window.xdg_toplevel);
|
|
zxdg_toplevel_decoration_v1_set_mode(
|
|
term.window.xdg_toplevel_decoration, ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE);
|
|
zxdg_toplevel_decoration_v1_add_listener(
|
|
term.window.xdg_toplevel_decoration, &xdg_toplevel_decoration_listener, term.wl);
|
|
|
|
/* Scrollback search box */
|
|
term.window.search_surface = wl_compositor_create_surface(term.wl->compositor);
|
|
term.window.search_sub_surface = wl_subcompositor_get_subsurface(
|
|
term.wl->sub_compositor, term.window.search_surface, term.window.surface);
|
|
wl_subsurface_set_desync(term.window.search_sub_surface);
|
|
|
|
wl_surface_commit(term.window.surface);
|
|
wl_display_roundtrip(term.wl->display);
|
|
|
|
if (conf.width == -1) {
|
|
assert(conf.height == -1);
|
|
conf.width = 80 * term.cell_width;
|
|
conf.height = 24 * term.cell_height;
|
|
}
|
|
conf.width = max(conf.width, term.cell_width);
|
|
conf.height = max(conf.height, term.cell_height);
|
|
render_resize(&term, conf.width, conf.height);
|
|
|
|
wl_display_dispatch_pending(term.wl->display);
|
|
|
|
{
|
|
int fork_pipe[2];
|
|
if (pipe2(fork_pipe, O_CLOEXEC) < 0) {
|
|
LOG_ERRNO("failed to create pipe");
|
|
goto out;
|
|
}
|
|
|
|
term.slave = fork();
|
|
switch (term.slave) {
|
|
case -1:
|
|
LOG_ERRNO("failed to fork");
|
|
close(fork_pipe[0]);
|
|
close(fork_pipe[1]);
|
|
goto out;
|
|
|
|
case 0:
|
|
/* Child */
|
|
close(fork_pipe[0]); /* Close read end */
|
|
|
|
char **_shell_argv = NULL;
|
|
char *const *shell_argv = argv;
|
|
|
|
if (argc == 0) {
|
|
if (!tokenize_cmdline(conf.shell, &_shell_argv)) {
|
|
(void)!write(fork_pipe[1], &errno, sizeof(errno));
|
|
_exit(0);
|
|
}
|
|
shell_argv = _shell_argv;
|
|
}
|
|
|
|
slave_spawn(term.ptmx, shell_argv, fork_pipe[1]);
|
|
assert(false);
|
|
break;
|
|
|
|
default: {
|
|
close(fork_pipe[1]); /* Close write end */
|
|
LOG_DBG("slave has PID %d", term.slave);
|
|
|
|
int _errno;
|
|
static_assert(sizeof(errno) == sizeof(_errno), "errno size mismatch");
|
|
|
|
ssize_t ret = read(fork_pipe[0], &_errno, sizeof(_errno));
|
|
close(fork_pipe[0]);
|
|
|
|
if (ret < 0) {
|
|
LOG_ERRNO("failed to read from pipe");
|
|
goto out;
|
|
} else if (ret == sizeof(_errno)) {
|
|
LOG_ERRNO(
|
|
"%s: failed to execute", argc == 0 ? conf.shell : argv[0]);
|
|
goto out;
|
|
} else
|
|
LOG_DBG("%s: successfully started", conf.shell);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Read logic requires non-blocking mode */
|
|
{
|
|
int fd_flags = fcntl(term.ptmx, F_GETFL);
|
|
if (fd_flags == -1) {
|
|
LOG_ERRNO("failed to set non blocking mode on PTY master");
|
|
goto out;
|
|
}
|
|
|
|
if (fcntl(term.ptmx, F_SETFL, fd_flags | O_NONBLOCK) == -1) {
|
|
LOG_ERRNO("failed to set non blocking mode on PTY master");
|
|
goto out;
|
|
}
|
|
}
|
|
|
|
|
|
{
|
|
int fd = wl_display_get_fd(term.wl->display);
|
|
int fd_flags = fcntl(fd, F_GETFL);
|
|
if (fd_flags == -1) {
|
|
LOG_ERRNO("failed to set non blocking mode on Wayland display connection");
|
|
goto out;
|
|
}
|
|
if (fcntl(fd, F_SETFL, fd_flags | O_NONBLOCK) == -1) {
|
|
LOG_ERRNO("failed to set non blocking mode on Wayland display connection");
|
|
goto out;
|
|
}
|
|
}
|
|
|
|
if ((fdm = fdm_init()) == NULL)
|
|
goto out;
|
|
|
|
fdm_add(fdm, wl_display_get_fd(term.wl->display), EPOLLIN, &fdm_wayl, term.wl);
|
|
fdm_add(fdm, term.ptmx, EPOLLIN, &fdm_ptmx, &term);
|
|
fdm_add(fdm, term.wl->kbd.repeat.fd, EPOLLIN, &fdm_repeat, term.wl);
|
|
fdm_add(fdm, term.flash.fd, EPOLLIN, &fdm_flash, &term);
|
|
fdm_add(fdm, term.blink.fd, EPOLLIN, &fdm_blink, &term);
|
|
fdm_add(fdm, term.delayed_render_timer.lower_fd, EPOLLIN, &fdm_delayed_render, &term);
|
|
fdm_add(fdm, term.delayed_render_timer.upper_fd, EPOLLIN, &fdm_delayed_render, &term);
|
|
|
|
while (true) {
|
|
wl_display_flush(term.wl->display);
|
|
if (!fdm_poll(fdm))
|
|
break;
|
|
}
|
|
|
|
if (term.quit)
|
|
ret = EXIT_SUCCESS;
|
|
|
|
out:
|
|
if (fdm != NULL) {
|
|
fdm_del(fdm, wl_display_get_fd(term.wl->display));
|
|
fdm_del(fdm, term.ptmx);
|
|
fdm_del(fdm, term.wl->kbd.repeat.fd);
|
|
fdm_del(fdm, term.flash.fd);
|
|
fdm_del(fdm, term.blink.fd);
|
|
fdm_del(fdm, term.delayed_render_timer.lower_fd);
|
|
fdm_del(fdm, term.delayed_render_timer.upper_fd);
|
|
}
|
|
|
|
if (term.delayed_render_timer.lower_fd != -1)
|
|
close(term.delayed_render_timer.lower_fd);
|
|
if (term.delayed_render_timer.upper_fd != -1)
|
|
close(term.delayed_render_timer.upper_fd);
|
|
|
|
mtx_lock(&term.render.workers.lock);
|
|
assert(tll_length(term.render.workers.queue) == 0);
|
|
for (size_t i = 0; i < term.render.workers.count; i++) {
|
|
sem_post(&term.render.workers.start);
|
|
tll_push_back(term.render.workers.queue, -2);
|
|
}
|
|
cnd_broadcast(&term.render.workers.cond);
|
|
mtx_unlock(&term.render.workers.lock);
|
|
|
|
shm_fini();
|
|
|
|
wayl_win_destroy(&term.window);
|
|
wayl_destroy(&wayl);
|
|
|
|
free(term.vt.osc.data);
|
|
for (int row = 0; row < term.normal.num_rows; row++)
|
|
grid_row_free(term.normal.rows[row]);
|
|
free(term.normal.rows);
|
|
for (int row = 0; row < term.alt.num_rows; row++)
|
|
grid_row_free(term.alt.rows[row]);
|
|
free(term.alt.rows);
|
|
|
|
free(term.window_title);
|
|
tll_free_and_free(term.window_title_stack, free);
|
|
|
|
for (size_t i = 0; i < sizeof(term.fonts) / sizeof(term.fonts[0]); i++)
|
|
font_destroy(term.fonts[i]);
|
|
|
|
free(term.search.buf);
|
|
|
|
if (term.flash.fd != -1)
|
|
close(term.flash.fd);
|
|
if (term.blink.fd != -1)
|
|
close(term.blink.fd);
|
|
if (term.wl->kbd.repeat.fd != -1)
|
|
close(term.wl->kbd.repeat.fd);
|
|
|
|
if (term.ptmx != -1)
|
|
close(term.ptmx);
|
|
|
|
for (size_t i = 0; i < term.render.workers.count; i++)
|
|
thrd_join(term.render.workers.threads[i], NULL);
|
|
free(term.render.workers.threads);
|
|
cnd_destroy(&term.render.workers.cond);
|
|
mtx_destroy(&term.render.workers.lock);
|
|
sem_destroy(&term.render.workers.start);
|
|
sem_destroy(&term.render.workers.done);
|
|
assert(tll_length(term.render.workers.queue) == 0);
|
|
tll_free(term.render.workers.queue);
|
|
|
|
if (term.slave > 0) {
|
|
/* Note: we've closed ptmx, so the slave *should* exit... */
|
|
int status;
|
|
waitpid(term.slave, &status, 0);
|
|
|
|
int child_ret = EXIT_FAILURE;
|
|
if (WIFEXITED(status)) {
|
|
child_ret = WEXITSTATUS(status);
|
|
LOG_DBG("slave exited with code %d", child_ret);
|
|
} else if (WIFSIGNALED(status)) {
|
|
child_ret = WTERMSIG(status);
|
|
LOG_WARN("slave exited with signal %d", child_ret);
|
|
} else {
|
|
LOG_WARN("slave exited for unknown reason (status = 0x%08x)", status);
|
|
}
|
|
|
|
if (ret == EXIT_SUCCESS)
|
|
ret = child_ret;
|
|
}
|
|
|
|
fdm_destroy(fdm);
|
|
|
|
config_free(conf);
|
|
return ret;
|
|
|
|
}
|