diff --git a/CHANGELOG.md b/CHANGELOG.md index b06dc6bf..c711b664 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/config.c b/config.c index ab9bb08b..283a72ea 100644 --- a/config.c +++ b/config.c @@ -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 diff --git a/config.h b/config.h index caf269b5..2b43b806 100644 --- a/config.h +++ b/config.h @@ -6,6 +6,7 @@ #include #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 { diff --git a/doc/foot.5.scd b/doc/foot.5.scd index 747c9cb3..92ac7c01 100644 --- a/doc/foot.5.scd +++ b/doc/foot.5.scd @@ -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. diff --git a/extract.c b/extract.c new file mode 100644 index 00000000..b626d3e6 --- /dev/null +++ b/extract.c @@ -0,0 +1,176 @@ +#include "extract.h" +#include + +#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; +} diff --git a/extract.h b/extract.h new file mode 100644 index 00000000..0b3693d2 --- /dev/null +++ b/extract.h @@ -0,0 +1,17 @@ +#pragma once + +#include +#include + +#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); diff --git a/footrc b/footrc index fe42c8eb..624c688b 100644 --- a/footrc +++ b/footrc @@ -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 diff --git a/input.c b/input.c index 79491f8f..0e4cb675 100644 --- a/input.c +++ b/input.c @@ -5,10 +5,12 @@ #include #include #include +#include #include #include #include #include +#include #include @@ -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; } } diff --git a/meson.build b/meson.build index a90a4ecb..a7db2105 100644 --- a/meson.build +++ b/meson.build @@ -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', diff --git a/selection.c b/selection.c index 165b7c46..d5add464 100644 --- a/selection.c +++ b/selection.c @@ -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 diff --git a/spawn.c b/spawn.c new file mode 100644 index 00000000..b268bfe5 --- /dev/null +++ b/spawn.c @@ -0,0 +1,76 @@ +#include "spawn.h" + +#include +#include +#include + +#include +#include +#include + +#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; +} diff --git a/spawn.h b/spawn.h new file mode 100644 index 00000000..2ab645a8 --- /dev/null +++ b/spawn.h @@ -0,0 +1,7 @@ +#pragma once + +#include +#include "reaper.h" + +bool spawn(struct reaper *reaper, const char *cwd, char *const argv[], + int stdin_fd, int stdout_fd, int stderr_fd); diff --git a/terminal.c b/terminal.c index 450b885b..86672f06 100644 --- a/terminal.c +++ b/terminal.c @@ -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); +} diff --git a/terminal.h b/terminal.h index c88bad2f..85f89abf 100644 --- a/terminal.h +++ b/terminal.h @@ -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); diff --git a/wayland.h b/wayland.h index 224853a6..dbf63c1b 100644 --- a/wayland.h +++ b/wayland.h @@ -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 {