Merge branch 'pipe-grid-to-external-tool'

Closes https://codeberg.org/dnkl/foot/issues/29
This commit is contained in:
Daniel Eklöf 2020-07-17 09:34:51 +02:00
commit ac42ce4303
No known key found for this signature in database
GPG key ID: 5BBD4992C116573F
15 changed files with 732 additions and 302 deletions

View file

@ -24,6 +24,10 @@
applications can override this.
* Multi-seat support
* Implemented `C0::FF` (form feed)
* **pipe-visible** and **pipe-scrollback** key bindings. These let you
pipe either the currently visible text, or the entire scrollback to
external tools (https://codeberg.org/dnkl/foot/issues/29). Example:
`pipe-visible=[sh -c "xurls | bemenu | xargs -r firefox] Control+Print`
### Changed

217
config.c
View file

@ -63,6 +63,8 @@ static const char *binding_action_map[] = {
[BIND_ACTION_MINIMIZE] = "minimize",
[BIND_ACTION_MAXIMIZE] = "maximize",
[BIND_ACTION_FULLSCREEN] = "fullscreen",
[BIND_ACTION_PIPE_SCROLLBACK] = "pipe-scrollback",
[BIND_ACTION_PIPE_VIEW] = "pipe-visible",
};
static_assert(ALEN(binding_action_map) == BIND_ACTION_COUNT,
@ -483,21 +485,19 @@ parse_section_csd(const char *key, const char *value, struct config *conf,
}
static bool
verify_key_combo(const struct config *conf, const char *combo, const char *path,
unsigned lineno)
verify_key_combo(const struct config *conf, enum bind_action_normal action,
const char *combo, const char *path, unsigned lineno)
{
for (enum bind_action_normal action = 0; action < BIND_ACTION_COUNT; action++) {
if (conf->bindings.key[action] == NULL)
continue;
char *copy = strdup(conf->bindings.key[action]);
tll_foreach(conf->bindings.key, it) {
char *copy = strdup(it->item.key);
for (char *save = NULL, *collision = strtok_r(copy, " ", &save);
collision != NULL;
collision = strtok_r(NULL, " ", &save))
{
if (strcmp(combo, collision) == 0) {
LOG_ERR("%s:%d: %s already mapped to %s", path, lineno, combo, binding_action_map[action]);
LOG_ERR("%s:%d: %s already mapped to %s", path, lineno, combo,
binding_action_map[it->item.action]);
free(copy);
return false;
}
@ -528,7 +528,26 @@ parse_section_key_bindings(
const char *key, const char *value, struct config *conf,
const char *path, unsigned lineno)
{
for (enum bind_action_normal action = 0; action < BIND_ACTION_COUNT; action++) {
const char *pipe_cmd = NULL;
size_t pipe_len = 0;
if (value[0] == '[') {
const char *pipe_cmd_end = strrchr(value, ']');
if (pipe_cmd_end == NULL) {
LOG_ERR("%s:%d: unclosed '['", path, lineno);
return false;
}
pipe_cmd = &value[1];
pipe_len = pipe_cmd_end - pipe_cmd;
value = pipe_cmd_end + 1;
}
for (enum bind_action_normal action = 0;
action < BIND_ACTION_COUNT;
action++)
{
if (binding_action_map[action] == NULL)
continue;
@ -536,17 +555,47 @@ parse_section_key_bindings(
continue;
if (strcasecmp(value, "none") == 0) {
free(conf->bindings.key[action]);
conf->bindings.key[action] = NULL;
tll_foreach(conf->bindings.key, it) {
if (it->item.action == action) {
free(it->item.key);
free(it->item.pipe_cmd);
tll_remove(conf->bindings.key, it);
}
}
return true;
}
if (!verify_key_combo(conf, value, path, lineno)) {
if (!verify_key_combo(conf, action, value, path, lineno)) {
return false;
}
free(conf->bindings.key[action]);
conf->bindings.key[action] = strdup(value);
bool already_added = false;
tll_foreach(conf->bindings.key, it) {
if (it->item.action == action &&
((it->item.pipe_cmd == NULL && pipe_cmd == NULL) ||
(it->item.pipe_cmd != NULL && pipe_cmd != NULL &&
strncmp(it->item.pipe_cmd, pipe_cmd, pipe_len) == 0)))
{
free(it->item.key);
free(it->item.pipe_cmd);
it->item.key = strdup(value);
it->item.pipe_cmd = pipe_cmd != NULL
? strndup(pipe_cmd, pipe_len) : NULL;
already_added = true;
break;
}
}
if (!already_added) {
struct config_key_binding_normal binding = {
.action = action,
.key = strdup(value),
.pipe_cmd = pipe_cmd != NULL ? strndup(pipe_cmd, pipe_len) : NULL,
};
tll_push_back(conf->bindings.key, binding);
}
return true;
}
@ -568,7 +617,12 @@ parse_section_mouse_bindings(
continue;
if (strcmp(value, "NONE") == 0) {
conf->bindings.mouse[action] = (struct mouse_binding){0, 0, BIND_ACTION_NONE};
tll_foreach(conf->bindings.mouse, it) {
if (it->item.action == action) {
tll_remove(conf->bindings.mouse, it);
break;
}
}
return true;
}
@ -590,16 +644,32 @@ parse_section_mouse_bindings(
const int count = 1;
/* Make sure button isn't already mapped to another action */
for (enum bind_action_normal j = 0; j < BIND_ACTION_COUNT; j++) {
const struct mouse_binding *collision = &conf->bindings.mouse[j];
if (collision->button == i && collision->count == count) {
tll_foreach(conf->bindings.mouse, it) {
if (it->item.button == i && it->item.count == count) {
LOG_ERR("%s:%d: %s already mapped to %s", path, lineno,
value, binding_action_map[collision->action]);
value, binding_action_map[it->item.action]);
return false;
}
}
conf->bindings.mouse[action] = (struct mouse_binding){i, count, action};
bool already_added = false;
tll_foreach(conf->bindings.mouse, it) {
if (it->item.action == action) {
it->item.button = i;
it->item.count = count;
already_added = true;
break;
}
}
if (!already_added) {
struct mouse_binding binding = {
.action = action,
.button = i,
.count = count,
};
tll_push_back(conf->bindings.mouse, binding);
}
return true;
}
@ -883,42 +953,6 @@ config_load(struct config *conf, const char *conf_path)
.cursor = 0,
},
},
.bindings = {
.key = {
[BIND_ACTION_SCROLLBACK_UP] = strdup("Shift+Page_Up"),
[BIND_ACTION_SCROLLBACK_DOWN] = strdup("Shift+Page_Down"),
[BIND_ACTION_CLIPBOARD_COPY] = strdup("Control+Shift+C"),
[BIND_ACTION_CLIPBOARD_PASTE] = strdup("Control+Shift+V"),
[BIND_ACTION_SEARCH_START] = strdup("Control+Shift+R"),
[BIND_ACTION_FONT_SIZE_UP] = strdup("Control+plus Control+equal Control+KP_Add"),
[BIND_ACTION_FONT_SIZE_DOWN] = strdup("Control+minus Control+KP_Subtract"),
[BIND_ACTION_FONT_SIZE_RESET] = strdup("Control+0 Control+KP_0"),
[BIND_ACTION_SPAWN_TERMINAL] = strdup("Control+Shift+N"),
},
.mouse = {
[BIND_ACTION_PRIMARY_PASTE] = {BTN_MIDDLE, 1, BIND_ACTION_PRIMARY_PASTE},
},
.search = {
[BIND_ACTION_SEARCH_CANCEL] = strdup("Control+g Escape"),
[BIND_ACTION_SEARCH_COMMIT] = strdup("Return"),
[BIND_ACTION_SEARCH_FIND_PREV] = strdup("Control+r"),
[BIND_ACTION_SEARCH_FIND_NEXT] = strdup("Control+s"),
[BIND_ACTION_SEARCH_EDIT_LEFT] = strdup("Left Control+b"),
[BIND_ACTION_SEARCH_EDIT_LEFT_WORD] = strdup("Control+Left Mod1+b"),
[BIND_ACTION_SEARCH_EDIT_RIGHT] = strdup("Right Control+f"),
[BIND_ACTION_SEARCH_EDIT_RIGHT_WORD] = strdup("Control+Right Mod1+f"),
[BIND_ACTION_SEARCH_EDIT_HOME] = strdup("Home Control+a"),
[BIND_ACTION_SEARCH_EDIT_END] = strdup("End Control+e"),
[BIND_ACTION_SEARCH_DELETE_PREV] = strdup("BackSpace"),
[BIND_ACTION_SEARCH_DELETE_PREV_WORD] = strdup("Mod1+BackSpace Control+BackSpace"),
[BIND_ACTION_SEARCH_DELETE_NEXT] = strdup("Delete "),
[BIND_ACTION_SEARCH_DELETE_NEXT_WORD] = strdup("Mod1+d Control+Delete"),
[BIND_ACTION_SEARCH_EXTEND_WORD] = strdup("Control+w"),
[BIND_ACTION_SEARCH_EXTEND_WORD_WS] = strdup("Control+Shift+W"),
},
},
.csd = {
.preferred = CONF_CSD_PREFER_SERVER,
.title_height = 26,
@ -938,6 +972,63 @@ config_load(struct config *conf, const char *conf_path)
},
};
struct config_key_binding_normal scrollback_up = {BIND_ACTION_SCROLLBACK_UP, strdup("Shift+Page_Up")};
struct config_key_binding_normal scrollback_down = {BIND_ACTION_SCROLLBACK_DOWN, strdup("Shift+Page_Down")};
struct config_key_binding_normal clipboard_copy = {BIND_ACTION_CLIPBOARD_COPY, strdup("Control+Shift+C")};
struct config_key_binding_normal clipboard_paste = {BIND_ACTION_CLIPBOARD_PASTE, strdup("Control+Shift+V")};
struct config_key_binding_normal search_start = {BIND_ACTION_SEARCH_START, strdup("Control+Shift+R")};
struct config_key_binding_normal font_size_up = {BIND_ACTION_FONT_SIZE_UP, strdup("Control+plus Control+equal Control+KP_Add")};
struct config_key_binding_normal font_size_down = {BIND_ACTION_FONT_SIZE_DOWN, strdup("Control+minus Control+KP_Subtract")};
struct config_key_binding_normal font_size_reset = {BIND_ACTION_FONT_SIZE_RESET, strdup("Control+0 Control+KP_0")};
struct config_key_binding_normal spawn_terminal = {BIND_ACTION_SPAWN_TERMINAL, strdup("Control+Shift+N")};
tll_push_back(conf->bindings.key, scrollback_up);
tll_push_back(conf->bindings.key, scrollback_down);
tll_push_back(conf->bindings.key, clipboard_copy);
tll_push_back(conf->bindings.key, clipboard_paste);
tll_push_back(conf->bindings.key, search_start);
tll_push_back(conf->bindings.key, font_size_up);
tll_push_back(conf->bindings.key, font_size_down);
tll_push_back(conf->bindings.key, font_size_reset);
tll_push_back(conf->bindings.key, spawn_terminal);
struct mouse_binding primary_paste = {BIND_ACTION_PRIMARY_PASTE, BTN_MIDDLE, 1};
tll_push_back(conf->bindings.mouse, primary_paste);
struct config_key_binding_search search_cancel = {BIND_ACTION_SEARCH_CANCEL, strdup("Control+g Escape")};
struct config_key_binding_search search_commit = {BIND_ACTION_SEARCH_COMMIT, strdup("Return")};
struct config_key_binding_search search_find_prev = {BIND_ACTION_SEARCH_FIND_PREV, strdup("Control+r")};
struct config_key_binding_search search_find_next = {BIND_ACTION_SEARCH_FIND_NEXT, strdup("Control+s")};
struct config_key_binding_search search_edit_left = {BIND_ACTION_SEARCH_EDIT_LEFT, strdup("Left Control+b")};
struct config_key_binding_search search_edit_left_word = {BIND_ACTION_SEARCH_EDIT_LEFT_WORD, strdup("Control+Left Mod1+b")};
struct config_key_binding_search search_edit_right = {BIND_ACTION_SEARCH_EDIT_RIGHT, strdup("Right Control+f")};
struct config_key_binding_search search_edit_right_word = {BIND_ACTION_SEARCH_EDIT_RIGHT_WORD, strdup("Control+Right Mod1+f")};
struct config_key_binding_search search_edit_home = {BIND_ACTION_SEARCH_EDIT_HOME, strdup("Home Control+a")};
struct config_key_binding_search search_edit_end = {BIND_ACTION_SEARCH_EDIT_END, strdup("End Control+e")};
struct config_key_binding_search search_del_prev = {BIND_ACTION_SEARCH_DELETE_PREV, strdup("BackSpace")};
struct config_key_binding_search search_del_prev_word = {BIND_ACTION_SEARCH_DELETE_PREV_WORD, strdup("Mod1+BackSpace Control+BackSpace")};
struct config_key_binding_search search_del_next = {BIND_ACTION_SEARCH_DELETE_NEXT, strdup("Delete")};
struct config_key_binding_search search_del_next_word = {BIND_ACTION_SEARCH_DELETE_NEXT_WORD, strdup("Mod1+d Control+Delete")};
struct config_key_binding_search search_ext_word = {BIND_ACTION_SEARCH_EXTEND_WORD, strdup("Control+w")};
struct config_key_binding_search search_ext_word_ws = {BIND_ACTION_SEARCH_EXTEND_WORD_WS, strdup("Control+Shift+W")};
tll_push_back(conf->bindings.search, search_cancel);
tll_push_back(conf->bindings.search, search_commit);
tll_push_back(conf->bindings.search, search_find_prev);
tll_push_back(conf->bindings.search, search_find_next);
tll_push_back(conf->bindings.search, search_edit_left);
tll_push_back(conf->bindings.search, search_edit_left_word);
tll_push_back(conf->bindings.search, search_edit_right);
tll_push_back(conf->bindings.search, search_edit_right_word);
tll_push_back(conf->bindings.search, search_edit_home);
tll_push_back(conf->bindings.search, search_edit_end);
tll_push_back(conf->bindings.search, search_del_prev);
tll_push_back(conf->bindings.search, search_del_prev_word);
tll_push_back(conf->bindings.search, search_del_next);
tll_push_back(conf->bindings.search, search_del_next_word);
tll_push_back(conf->bindings.search, search_ext_word);
tll_push_back(conf->bindings.search, search_ext_word_ws);
char *default_path = NULL;
if (conf_path == NULL) {
if ((default_path = get_config_path()) == NULL) {
@ -982,10 +1073,16 @@ config_free(struct config conf)
tll_free(conf.fonts);
free(conf.server_socket_path);
for (enum bind_action_normal i = 0; i < BIND_ACTION_COUNT; i++)
free(conf.bindings.key[i]);
for (enum bind_action_search i = 0; i < BIND_ACTION_SEARCH_COUNT; i++)
free(conf.bindings.search[i]);
tll_foreach(conf.bindings.key, it) {
free(it->item.key);
free(it->item.pipe_cmd);
}
tll_foreach(conf.bindings.search, it)
free(it->item.key);
tll_free(conf.bindings.key);
tll_free(conf.bindings.mouse);
tll_free(conf.bindings.search);
}
struct config_font

View file

@ -6,6 +6,7 @@
#include <tllist.h>
#include "terminal.h"
#include "wayland.h"
struct config_font {
char *pattern;
@ -13,6 +14,17 @@ struct config_font {
int px_size;
};
struct config_key_binding_normal {
enum bind_action_normal action;
char *key;
char *pipe_cmd;
};
struct config_key_binding_search {
enum bind_action_search action;
char *key;
};
struct config {
char *term;
char *shell;
@ -48,8 +60,8 @@ struct config {
struct {
/* Bindings for "normal" mode */
char *key[BIND_ACTION_COUNT];
struct mouse_binding mouse[BIND_ACTION_COUNT];
tll(struct config_key_binding_normal) key;
tll(struct mouse_binding) mouse;
/*
* Special modes
@ -57,7 +69,7 @@ struct config {
/* While searching (not - action to *start* a search is in the
* 'key' bindings above */
char *search[BIND_ACTION_SEARCH_COUNT];
tll(struct config_key_binding_search) search;
} bindings;
struct {

View file

@ -234,6 +234,23 @@ e.g. *search-start=none*.
*fullscreen*
Toggles the fullscreen state. Default: _not bound_.
*pipe-visible*, *pipe-scrollback*
Pipes the currently visible text, or the entire scrollback, to an
external tool. The syntax for this option is a bit special; the
first part of the value is the command to execute enclosed in
"[]", followed by the binding(s).
You can configure multiple pipes as long as the command strings
are different and the key bindings are unique.
Note that the command is *not* automatically run inside a shell;
use *sh -c "command line"* if you need that.
Example:
*pipe-visible=[sh -c "xurls | bemenu | xargs -r firefox"] Control+Print*
Default: _not bound_
# SECTION: mouse-bindings
This section lets you override the default mouse bindings.

176
extract.c Normal file
View file

@ -0,0 +1,176 @@
#include "extract.h"
#include <stdlib.h>
#define LOG_MODULE "extract"
#define LOG_ENABLE_DBG 1
#include "log.h"
struct extraction_context {
wchar_t *buf;
size_t size;
size_t idx;
size_t empty_count;
bool failed;
const struct row *last_row;
const struct cell *last_cell;
enum selection_kind selection_kind;
};
struct extraction_context *
extract_begin(enum selection_kind kind)
{
struct extraction_context *ctx = malloc(sizeof(*ctx));
*ctx = (struct extraction_context){
.selection_kind = kind,
};
return ctx;
}
static bool
ensure_size(struct extraction_context *ctx, size_t additional_chars)
{
while (ctx->size < ctx->idx + additional_chars) {
size_t new_size = ctx->size == 0 ? 512 : ctx->size * 2;
wchar_t *new_buf = realloc(ctx->buf, new_size * sizeof(wchar_t));
if (new_buf == NULL)
return false;
ctx->buf = new_buf;
ctx->size = new_size;
}
assert(ctx->size >= ctx->idx + additional_chars);
return true;
}
bool
extract_finish(struct extraction_context *ctx, char **text, size_t *len)
{
bool ret = false;
if (text == NULL)
return false;
*text = NULL;
if (len != NULL)
*len = 0;
if (ctx->failed)
goto out;
if (ctx->idx == 0) {
/* Selection of empty cells only */
if (!ensure_size(ctx, 1))
goto out;
ctx->buf[ctx->idx] = L'\0';
} else {
assert(ctx->idx > 0);
assert(ctx->idx < ctx->size);
if (ctx->buf[ctx->idx - 1] == L'\n')
ctx->buf[ctx->idx - 1] = L'\0';
else
ctx->buf[ctx->idx] = L'\0';
}
size_t _len = wcstombs(NULL, ctx->buf, 0);
if (_len == (size_t)-1) {
LOG_ERRNO("failed to convert selection to UTF-8");
goto out;
}
*text = malloc(_len + 1);
wcstombs(*text, ctx->buf, _len + 1);
if (len != NULL)
*len = _len;
ret = true;
out:
free(ctx->buf);
free(ctx);
return ret;
}
bool
extract_one(const struct terminal *term, const struct row *row,
const struct cell *cell, int col, void *context)
{
struct extraction_context *ctx = context;
if (cell->wc == CELL_MULT_COL_SPACER)
return true;
if (ctx->last_row != NULL && row != ctx->last_row) {
/* New row - determine if we should insert a newline or not */
if (ctx->selection_kind == SELECTION_NONE ||
ctx->selection_kind == SELECTION_NORMAL)
{
if (ctx->last_row->linebreak ||
ctx->empty_count > 0 ||
cell->wc == 0)
{
/* Row has a hard linebreak, or either last cell or
* current cell is empty */
if (!ensure_size(ctx, 1))
goto err;
ctx->buf[ctx->idx++] = L'\n';
ctx->empty_count = 0;
}
}
else if (ctx->selection_kind == SELECTION_BLOCK) {
/* Always insert a linebreak */
if (!ensure_size(ctx, 1))
goto err;
ctx->buf[ctx->idx++] = L'\n';
ctx->empty_count = 0;
}
}
if (cell->wc == 0) {
ctx->empty_count++;
ctx->last_row = row;
ctx->last_cell = cell;
return true;
}
/* Replace empty cells with spaces when followed by non-empty cell */
if (!ensure_size(ctx, ctx->empty_count))
goto err;
for (size_t i = 0; i < ctx->empty_count; i++)
ctx->buf[ctx->idx++] = L' ';
ctx->empty_count = 0;
if (cell->wc >= CELL_COMB_CHARS_LO &&
cell->wc < (CELL_COMB_CHARS_LO + term->composed_count))
{
const struct composed *composed
= &term->composed[cell->wc - CELL_COMB_CHARS_LO];
if (!ensure_size(ctx, 1 + composed->count))
goto err;
ctx->buf[ctx->idx++] = composed->base;
for (size_t i = 0; i < composed->count; i++)
ctx->buf[ctx->idx++] = composed->combining[i];
}
else {
if (!ensure_size(ctx, 1))
goto err;
ctx->buf[ctx->idx++] = cell->wc;
}
ctx->last_row = row;
ctx->last_cell = cell;
return true;
err:
ctx->failed = true;
return false;
}

17
extract.h Normal file
View file

@ -0,0 +1,17 @@
#pragma once
#include <stddef.h>
#include <stdbool.h>
#include "terminal.h"
struct extraction_context;
struct extraction_context *extract_begin(enum selection_kind kind);
bool extract_one(
const struct terminal *term, const struct row *row, const struct cell *cell,
int col, void *context);
bool extract_finish(
struct extraction_context *context, char **text, size_t *len);

2
footrc
View file

@ -58,6 +58,8 @@
# minimize=none
# maximize=none
# fullscreen=none
# pipe-visible=[sh -c "xurls | bemenu | xargs -r firefox"] none
# pipe-scrollback=[sh -c "xurls | bemenu | xargs -r firefox"] none
[mouse-bindings]
# primary-paste=BTN_MIDDLE

179
input.c
View file

@ -5,10 +5,12 @@
#include <signal.h>
#include <threads.h>
#include <locale.h>
#include <errno.h>
#include <sys/mman.h>
#include <sys/time.h>
#include <sys/timerfd.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <linux/input-event-codes.h>
@ -28,13 +30,54 @@
#include "render.h"
#include "search.h"
#include "selection.h"
#include "spawn.h"
#include "terminal.h"
#include "tokenize.h"
#include "util.h"
#include "vt.h"
struct pipe_context {
char *text;
size_t idx;
size_t left;
};
static bool
fdm_write_pipe(struct fdm *fdm, int fd, int events, void *data)
{
struct pipe_context *ctx = data;
if (events & EPOLLHUP)
goto pipe_closed;
assert(events & EPOLLOUT);
ssize_t written = write(fd, &ctx->text[ctx->idx], ctx->left);
if (written < 0) {
LOG_WARN("failed to write to pipe: %s", strerror(errno));
goto pipe_closed;
}
assert(written <= ctx->left);
ctx->idx += written;
ctx->left -= written;
if (ctx->left == 0)
goto pipe_closed;
return true;
pipe_closed:
free(ctx->text);
free(ctx);
fdm_del(fdm, fd);
return true;
}
static void
execute_binding(struct seat *seat, struct terminal *term,
enum bind_action_normal action, uint32_t serial)
enum bind_action_normal action, const char *pipe_cmd,
uint32_t serial)
{
switch (action) {
case BIND_ACTION_NONE:
@ -101,6 +144,104 @@ execute_binding(struct seat *seat, struct terminal *term,
xdg_toplevel_set_fullscreen(term->window->xdg_toplevel, NULL);
break;
case BIND_ACTION_PIPE_SCROLLBACK:
case BIND_ACTION_PIPE_VIEW: {
if (pipe_cmd == NULL)
break;
struct pipe_context *ctx = NULL;
char *cmd = strdup(pipe_cmd);
char **argv = NULL;
if (!tokenize_cmdline(cmd, &argv))
goto pipe_err;
int pipe_fd[2] = {-1, -1};
if (pipe(pipe_fd) < 0) {
LOG_ERRNO("failed to create pipe");
goto pipe_err;
}
int stdout_fd = open("/dev/null", O_WRONLY);
int stderr_fd = open("/dev/null", O_WRONLY);
if (stdout_fd < 0 || stderr_fd < 0) {
LOG_ERRNO("failed to open /dev/null");
goto pipe_err;
}
char *text;
size_t len;
bool success = action == BIND_ACTION_PIPE_SCROLLBACK
? term_scrollback_to_text(term, &text, &len)
: term_view_to_text(term, &text, &len);
if (!success)
goto pipe_err;
/* Make write-end non-blocking; required by the FDM */
{
int flags = fcntl(pipe_fd[1], F_GETFL);
if (flags < 0 ||
fcntl(pipe_fd[1], F_SETFL, flags | O_NONBLOCK) < 0)
{
LOG_ERRNO("failed to make write-end of pipe non-blocking");
goto pipe_err;
}
}
/* Make sure write-end is closed on exec() - or the spawned
* program may not terminate*/
{
int flags = fcntl(pipe_fd[1], F_GETFD);
if (flags < 0 ||
fcntl(pipe_fd[1], F_SETFD, flags | FD_CLOEXEC) < 0)
{
LOG_ERRNO("failed to set FD_CLOEXEC on writeend of pipe");
goto pipe_err;
}
}
if (!spawn(term->reaper, NULL, argv, pipe_fd[0], stdout_fd, stderr_fd))
goto pipe_err;
/* Not needed anymore */
free(argv); argv = NULL;
free(cmd); cmd = NULL;
/* Close read end */
close(pipe_fd[0]);
ctx = malloc(sizeof(*ctx));
*ctx = (struct pipe_context){
.text = text,
.left = len,
};
/* Asynchronously write the output to the pipe */
if (!fdm_add(term->fdm, pipe_fd[1], EPOLLOUT, &fdm_write_pipe, ctx))
goto pipe_err;
break;
pipe_err:
if (stdout_fd >= 0)
close(stdout_fd);
if (stderr_fd >= 0)
close(stderr_fd);
if (pipe_fd[0] >= 0)
close(pipe_fd[0]);
if (pipe_fd[1] >= 0)
close(pipe_fd[1]);
free(text);
free(argv);
free(cmd);
free(ctx);
break;
}
case BIND_ACTION_COUNT:
assert(false);
break;
@ -192,7 +333,8 @@ input_parse_key_binding(struct xkb_keymap *keymap, const char *combos,
};
tll_push_back(*bindings, binding);
}
} else
tll_free(key_codes);
}
free(copy);
@ -270,31 +412,33 @@ keyboard_keymap(void *data, struct wl_keyboard *wl_keyboard,
munmap(map_str, size);
close(fd);
for (enum bind_action_normal i = 0; i < BIND_ACTION_COUNT; i++) {
tll_foreach(wayl->conf->bindings.key, it) {
key_binding_list_t bindings = tll_init();
input_parse_key_binding(
seat->kbd.xkb_keymap, wayl->conf->bindings.key[i], &bindings);
seat->kbd.xkb_keymap, it->item.key, &bindings);
tll_foreach(bindings, it) {
tll_foreach(bindings, it2) {
tll_push_back(
seat->kbd.bindings.key,
((struct key_binding_normal){.bind = it->item, .action = i}));
((struct key_binding_normal){
.bind = it2->item,
.action = it->item.action,
.pipe_cmd = it->item.pipe_cmd}));
}
tll_free(bindings);
}
for (enum bind_action_search i = 0; i < BIND_ACTION_SEARCH_COUNT; i++) {
tll_foreach(wayl->conf->bindings.search, it) {
key_binding_list_t bindings = tll_init();
input_parse_key_binding(
seat->kbd.xkb_keymap, wayl->conf->bindings.search[i], &bindings);
seat->kbd.xkb_keymap, it->item.key, &bindings);
tll_foreach(bindings, it) {
tll_foreach(bindings, it2) {
tll_push_back(
seat->kbd.bindings.search,
((struct key_binding_search){.bind = it->item, .action = i}));
((struct key_binding_search){
.bind = it2->item, .action = it->item.action}));
}
tll_free(bindings);
}
}
@ -596,14 +740,14 @@ keyboard_key(void *data, struct wl_keyboard *wl_keyboard, uint32_t serial,
/* Match symbol */
if (it->item.bind.sym == sym) {
execute_binding(seat, term, it->item.action, serial);
execute_binding(seat, term, it->item.action, it->item.pipe_cmd, serial);
goto maybe_repeat;
}
/* Match raw key code */
tll_foreach(it->item.bind.key_codes, code) {
if (code->item == key) {
execute_binding(seat, term, it->item.action, serial);
execute_binding(seat, term, it->item.action, it->item.pipe_cmd, serial);
goto maybe_repeat;
}
}
@ -1239,9 +1383,8 @@ wl_pointer_button(void *data, struct wl_pointer *wl_pointer,
}
else {
for (size_t i = 0; i < ALEN(wayl->conf->bindings.mouse); i++) {
const struct mouse_binding *binding =
&wayl->conf->bindings.mouse[i];
tll_foreach(wayl->conf->bindings.mouse, it) {
const struct mouse_binding *binding = &it->item;
if (binding->button != button) {
/* Wrong button */
@ -1253,7 +1396,7 @@ wl_pointer_button(void *data, struct wl_pointer *wl_pointer,
continue;
}
execute_binding(seat, term, binding->action, serial);
execute_binding(seat, term, binding->action, NULL, serial);
break;
}
}

View file

@ -105,6 +105,7 @@ executable(
'commands.c', 'commands.h',
'csi.c', 'csi.h',
'dcs.c', 'dcs.h',
'extract.c', 'extract.h',
'fdm.c', 'fdm.h',
'grid.c', 'grid.h',
'input.c', 'input.h',
@ -122,6 +123,7 @@ executable(
'sixel.c', 'sixel.h',
'sixel-hls.c', 'sixel-hls.h',
'slave.c', 'slave.h',
'spawn.c', 'spawn.h',
'terminal.c', 'terminal.h',
'tokenize.c', 'tokenize.h',
'vt.c', 'vt.h',

View file

@ -14,6 +14,7 @@
#include "log.h"
#include "async.h"
#include "extract.h"
#include "grid.h"
#include "misc.h"
#include "render.h"
@ -100,7 +101,7 @@ selection_view_down(struct terminal *term, int new_view)
static void
foreach_selected_normal(
struct terminal *term, struct coord _start, struct coord _end,
void (*cb)(struct terminal *term, struct row *row, struct cell *cell, int col, void *data),
bool (*cb)(struct terminal *term, struct row *row, struct cell *cell, int col, void *data),
void *data)
{
const struct coord *start = &_start;
@ -134,7 +135,8 @@ foreach_selected_normal(
c <= (r == end_row ? end_col : term->cols - 1);
c++)
{
cb(term, row, &row->cells[c], c, data);
if (!cb(term, row, &row->cells[c], c, data))
return;
}
start_col = 0;
@ -144,7 +146,7 @@ foreach_selected_normal(
static void
foreach_selected_block(
struct terminal *term, struct coord _start, struct coord _end,
void (*cb)(struct terminal *term, struct row *row, struct cell *cell, int col, void *data),
bool (*cb)(struct terminal *term, struct row *row, struct cell *cell, int col, void *data),
void *data)
{
const struct coord *start = &_start;
@ -166,7 +168,8 @@ foreach_selected_block(
assert(row != NULL);
for (int c = top_left.col; c <= bottom_right.col; c++) {
cb(term, row, &row->cells[c], c, data);
if (!cb(term, row, &row->cells[c], c, data))
return;
}
}
}
@ -174,7 +177,7 @@ foreach_selected_block(
static void
foreach_selected(
struct terminal *term, struct coord start, struct coord end,
void (*cb)(struct terminal *term, struct row *row, struct cell *cell, int col, void *data),
bool (*cb)(struct terminal *term, struct row *row, struct cell *cell, int col, void *data),
void *data)
{
switch (term->selection.kind) {
@ -192,173 +195,27 @@ foreach_selected(
assert(false);
}
static size_t
min_bufsize_for_extraction(const struct terminal *term)
static bool
extract_one_const_wrapper(struct terminal *term,
struct row *row, struct cell *cell,
int col, void *data)
{
const struct coord *start = &term->selection.start;
const struct coord *end = &term->selection.end;
const size_t chars_per_cell = 1 + ALEN(term->composed[0].combining);
switch (term->selection.kind) {
case SELECTION_NONE:
return 0;
case SELECTION_NORMAL:
if (term->selection.end.row < 0)
return 0;
assert(term->selection.start.row != -1);
if (start->row > end->row) {
const struct coord *tmp = start;
start = end;
end = tmp;
}
if (start->row == end->row)
return (end->col - start->col + 1) * chars_per_cell;
else {
size_t cells = 0;
/* Add one extra column on each row, for \n */
cells += term->cols - start->col + 1;
cells += (term->cols + 1) * (end->row - start->row - 1);
cells += end->col + 1 + 1;
return cells * chars_per_cell;
}
case SELECTION_BLOCK: {
struct coord top_left = {
.row = min(start->row, end->row),
.col = min(start->col, end->col),
};
struct coord bottom_right = {
.row = max(start->row, end->row),
.col = max(start->col, end->col),
};
/* Add one extra column on each row, for \n */
int cols = bottom_right.col - top_left.col + 1 + 1;
int rows = bottom_right.row - top_left.row + 1;
return rows * cols * chars_per_cell;
}
}
assert(false);
return 0;
}
struct extract {
wchar_t *buf;
size_t size;
size_t idx;
size_t empty_count;
const struct row *last_row;
const struct cell *last_cell;
};
static void
extract_one(struct terminal *term, struct row *row, struct cell *cell,
int col, void *data)
{
struct extract *ctx = data;
if (cell->wc == CELL_MULT_COL_SPACER)
return;
if (ctx->last_row != NULL && row != ctx->last_row) {
/* New row - determine if we should insert a newline or not */
if (term->selection.kind == SELECTION_NORMAL) {
if (ctx->last_row->linebreak ||
ctx->empty_count > 0 ||
cell->wc == 0)
{
/* Row has a hard linebreak, or either last cell or
* current cell is empty */
ctx->buf[ctx->idx++] = L'\n';
ctx->empty_count = 0;
}
}
else if (term->selection.kind == SELECTION_BLOCK) {
/* Always insert a linebreak */
ctx->buf[ctx->idx++] = L'\n';
ctx->empty_count = 0;
}
}
if (cell->wc == 0) {
ctx->empty_count++;
ctx->last_row = row;
ctx->last_cell = cell;
return;
}
/* Replace empty cells with spaces when followed by non-empty cell */
assert(ctx->idx + ctx->empty_count <= ctx->size);
for (size_t i = 0; i < ctx->empty_count; i++)
ctx->buf[ctx->idx++] = L' ';
ctx->empty_count = 0;
assert(ctx->idx + 1 <= ctx->size);
if (cell->wc >= CELL_COMB_CHARS_LO &&
cell->wc < (CELL_COMB_CHARS_LO + term->composed_count)) {
const struct composed *composed = &term->composed[cell->wc - CELL_COMB_CHARS_LO];
ctx->buf[ctx->idx++] = composed->base;
assert(ctx->idx + composed->count <= ctx->size);
for (size_t i = 0; i < composed->count; i++)
ctx->buf[ctx->idx++] = composed->combining[i];
} else
ctx->buf[ctx->idx++] = cell->wc;
ctx->last_row = row;
ctx->last_cell = cell;
return extract_one(term, row, cell, col, data);
}
static char *
extract_selection(const struct terminal *term)
{
const size_t max_cells = min_bufsize_for_extraction(term);
const size_t buf_size = max_cells + 1;
struct extract ctx = {
.buf = malloc(buf_size * sizeof(wchar_t)),
.size = buf_size,
};
struct extraction_context *ctx = extract_begin(term->selection.kind);
if (ctx == NULL)
return NULL;
foreach_selected(
(struct terminal *)term, term->selection.start, term->selection.end,
&extract_one, &ctx);
&extract_one_const_wrapper, ctx);
if (ctx.idx == 0) {
/* Selection of empty cells only */
ctx.buf[ctx.idx] = L'\0';
} else {
assert(ctx.idx > 0);
assert(ctx.idx < ctx.size);
if (ctx.buf[ctx.idx - 1] == L'\n')
ctx.buf[ctx.idx - 1] = L'\0';
else
ctx.buf[ctx.idx] = L'\0';
}
size_t len = wcstombs(NULL, ctx.buf, 0);
if (len == (size_t)-1) {
LOG_ERRNO("failed to convert selection to UTF-8");
free(ctx.buf);
return NULL;
}
char *ret = malloc(len + 1);
wcstombs(ret, ctx.buf, len + 1);
free(ctx.buf);
return ret;
char *text;
return extract_finish(ctx, &text, NULL) ? text : NULL;
}
void
@ -377,41 +234,44 @@ selection_start(struct terminal *term, int col, int row,
term->selection.end = (struct coord){-1, -1};
}
static void
static bool
unmark_selected(struct terminal *term, struct row *row, struct cell *cell,
int col, void *data)
{
if (cell->attrs.selected == 0 || (cell->attrs.selected & 2)) {
/* Ignore if already deselected, or if premarked for updated selection */
return;
return true;
}
row->dirty = true;
cell->attrs.selected = 0;
cell->attrs.clean = 0;
return true;
}
static void
static bool
premark_selected(struct terminal *term, struct row *row, struct cell *cell,
int col, void *data)
{
/* Tell unmark to leave this be */
cell->attrs.selected |= 2;
return true;
}
static void
static bool
mark_selected(struct terminal *term, struct row *row, struct cell *cell,
int col, void *data)
{
if (cell->attrs.selected & 1) {
cell->attrs.selected = 1; /* Clear the pre-mark bit */
return;
return true;
}
row->dirty = true;
cell->attrs.selected = 1;
cell->attrs.clean = 0;
return true;
}
static void

76
spawn.c Normal file
View file

@ -0,0 +1,76 @@
#include "spawn.h"
#include <unistd.h>
#include <errno.h>
#include <assert.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define LOG_MODULE "spawn"
#define LOG_ENABLE_DBG 0
#include "log.h"
bool
spawn(struct reaper *reaper, const char *cwd, char *const argv[],
int stdin_fd, int stdout_fd, int stderr_fd)
{
int pipe_fds[2] = {-1, -1};
if (pipe2(pipe_fds, O_CLOEXEC) < 0) {
LOG_ERRNO("failed to create pipe");
goto err;
}
pid_t pid = fork();
if (pid < 0) {
LOG_ERRNO("failed to fork");
goto err;
}
if (pid == 0) {
/* Child */
close(pipe_fds[0]);
if ((stdin_fd >= 0 && (dup2(stdin_fd, STDIN_FILENO) < 0 || close(stdin_fd) < 0)) ||
(stdout_fd >= 0 && (dup2(stdout_fd, STDOUT_FILENO) < 0 || close(stdout_fd) < 0)) ||
(stderr_fd >= 0 && (dup2(stderr_fd, STDERR_FILENO) < 0 || close(stderr_fd) < 0)) ||
(cwd != NULL && chdir(cwd) < 0) ||
execvp(argv[0], argv) < 0)
{
(void)!write(pipe_fds[1], &errno, sizeof(errno));
_exit(errno);
}
assert(false);
_exit(errno);
}
/* Parent */
close(pipe_fds[1]);
int _errno;
static_assert(sizeof(_errno) == sizeof(errno), "errno size mismatch");
ssize_t ret = read(pipe_fds[0], &_errno, sizeof(_errno));
close(pipe_fds[0]);
if (ret == 0) {
reaper_add(reaper, pid);
return true;
} else if (ret < 0) {
LOG_ERRNO("failed to read from pipe");
return false;
} else {
LOG_ERRNO_P("%s: failed to spawn", _errno, argv[0]);
errno = _errno;
waitpid(pid, NULL, 0);
return false;
}
err:
if (pipe_fds[0] != -1)
close(pipe_fds[0]);
if (pipe_fds[1] != -1)
close(pipe_fds[1]);
return false;
}

7
spawn.h Normal file
View file

@ -0,0 +1,7 @@
#pragma once
#include <stdbool.h>
#include "reaper.h"
bool spawn(struct reaper *reaper, const char *cwd, char *const argv[],
int stdin_fd, int stdout_fd, int stderr_fd);

View file

@ -22,6 +22,7 @@
#include "async.h"
#include "config.h"
#include "extract.h"
#include "grid.h"
#include "quirks.h"
#include "reaper.h"
@ -29,6 +30,7 @@
#include "selection.h"
#include "sixel.h"
#include "slave.h"
#include "spawn.h"
#include "util.h"
#include "vt.h"
@ -2245,59 +2247,9 @@ term_flash(struct terminal *term, unsigned duration_ms)
bool
term_spawn_new(const struct terminal *term)
{
int pipe_fds[2] = {-1, -1};
if (pipe2(pipe_fds, O_CLOEXEC) < 0) {
LOG_ERRNO("failed to create pipe");
goto err;
}
pid_t pid = fork();
if (pid < 0) {
LOG_ERRNO("failed to fork new terminal");
goto err;
}
if (pid == 0) {
/* Child */
close(pipe_fds[0]);
if (chdir(term->cwd) < 0 ||
execlp(term->foot_exe, term->foot_exe, NULL) < 0)
{
(void)!write(pipe_fds[1], &errno, sizeof(errno));
_exit(errno);
}
assert(false);
_exit(errno);
}
/* Parent */
close(pipe_fds[1]);
int _errno;
static_assert(sizeof(_errno) == sizeof(errno), "errno size mismatch");
ssize_t ret = read(pipe_fds[0], &_errno, sizeof(_errno));
close(pipe_fds[0]);
if (ret == 0) {
reaper_add(term->reaper, pid);
return true;
} else if (ret < 0) {
LOG_ERRNO("failed to read from pipe");
return false;
} else {
LOG_ERRNO_P("%s: failed to spawn new terminal", _errno, term->foot_exe);
errno = _errno;
waitpid(pid, NULL, 0);
return false;
}
err:
if (pipe_fds[0] != -1)
close(pipe_fds[0]);
if (pipe_fds[1] != -1)
close(pipe_fds[1]);
return false;
return spawn(
term->reaper, term->cwd, (char *const []){term->foot_exe, NULL},
-1, -1, -1);
}
void
@ -2474,3 +2426,60 @@ term_surface_kind(const struct terminal *term, const struct wl_surface *surface)
else
return TERM_SURF_NONE;
}
static bool
rows_to_text(const struct terminal *term, int start, int end,
char **text, size_t *len)
{
struct extraction_context *ctx = extract_begin(SELECTION_NONE);
if (ctx == NULL)
return false;
for (size_t r = start;
r != ((end + 1) & (term->grid->num_rows - 1));
r = (r + 1) & (term->grid->num_rows - 1))
{
const struct row *row = term->grid->rows[r];
assert(row != NULL);
for (int c = 0; c < term->cols; c++)
if (!extract_one(term, row, &row->cells[c], c, ctx))
goto out;
}
out:
return extract_finish(ctx, text, len);
}
bool
term_scrollback_to_text(const struct terminal *term, char **text, size_t *len)
{
int start = term->grid->offset + term->rows;
int end = term->grid->offset + term->rows - 1;
/* 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 &= term->grid->num_rows - 1;
}
if (end < 0)
end += term->grid->num_rows;
while (term->grid->rows[end] == NULL) {
end--;
if (end < 0)
end += term->grid->num_rows;
}
return rows_to_text(term, start, end, 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, text, len);
}

View file

@ -540,3 +540,8 @@ void term_disable_app_sync_updates(struct terminal *term);
enum term_surface term_surface_kind(
const struct terminal *term, const struct wl_surface *surface);
bool term_scrollback_to_text(
const struct terminal *term, char **text, size_t *len);
bool term_view_to_text(
const struct terminal *term, char **text, size_t *len);

View file

@ -39,18 +39,21 @@ enum bind_action_normal {
BIND_ACTION_MINIMIZE,
BIND_ACTION_MAXIMIZE,
BIND_ACTION_FULLSCREEN,
BIND_ACTION_PIPE_SCROLLBACK,
BIND_ACTION_PIPE_VIEW,
BIND_ACTION_COUNT,
};
struct key_binding_normal {
struct key_binding bind;
enum bind_action_normal action;
const char *pipe_cmd;
};
struct mouse_binding {
enum bind_action_normal action;
uint32_t button;
int count;
enum bind_action_normal action;
};
enum bind_action_search {