From 85c50099afa792a45671664275d3cd33bb8606e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 15 Jul 2020 09:46:13 +0200 Subject: [PATCH 01/23] conf: add definitions for pipe-{scrollback,view} bindings --- config.c | 2 ++ input.c | 5 +++++ wayland.h | 2 ++ 3 files changed, 9 insertions(+) diff --git a/config.c b/config.c index ab9bb08b..f8be377d 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, diff --git a/input.c b/input.c index 79491f8f..2df8c73a 100644 --- a/input.c +++ b/input.c @@ -101,6 +101,11 @@ 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: + LOG_ERR("unimplemented"); + break; + case BIND_ACTION_COUNT: assert(false); break; diff --git a/wayland.h b/wayland.h index 224853a6..c47d3c29 100644 --- a/wayland.h +++ b/wayland.h @@ -39,6 +39,8 @@ enum bind_action_normal { BIND_ACTION_MINIMIZE, BIND_ACTION_MAXIMIZE, BIND_ACTION_FULLSCREEN, + BIND_ACTION_PIPE_SCROLLBACK, + BIND_ACTION_PIPE_VIEW, BIND_ACTION_COUNT, }; From aafa120f92b2f41d1a753493676ee7ee0cb7a1ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 15 Jul 2020 11:19:18 +0200 Subject: [PATCH 02/23] selection: refactor: break out text extraction to a separate file --- extract.c | 163 ++++++++++++++++++++++++++++++++++++++++++++ extract.h | 17 +++++ meson.build | 1 + selection.c | 192 ++++++---------------------------------------------- 4 files changed, 203 insertions(+), 170 deletions(-) create mode 100644 extract.c create mode 100644 extract.h diff --git a/extract.c b/extract.c new file mode 100644 index 00000000..b119bc3f --- /dev/null +++ b/extract.c @@ -0,0 +1,163 @@ +#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; + 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; +} + +bool +extract_finish(struct extraction_context *ctx, char **text, size_t *len) +{ + if (text == NULL) + return false; + + 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'; + } + + bool ret = false; + + 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; +} + +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_one(struct terminal *term, struct row *row, 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)) + return false; + + 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)) + return false; + + 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)) + return false; + + 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)) + return false; + + 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)) + return false; + ctx->buf[ctx->idx++] = cell->wc; + } + + ctx->last_row = row; + ctx->last_cell = cell; + return true; +} diff --git a/extract.h b/extract.h new file mode 100644 index 00000000..2219780d --- /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( + struct terminal *term, struct row *row, struct cell *cell, int col, + void *context); + +bool extract_finish( + struct extraction_context *context, char **text, size_t *len); diff --git a/meson.build b/meson.build index a90a4ecb..a64f239f 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', diff --git a/selection.c b/selection.c index 165b7c46..eb21d318 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,19 @@ foreach_selected( assert(false); } -static size_t -min_bufsize_for_extraction(const struct terminal *term) -{ - 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; -} - 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, 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 +226,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 From 2539e3cbb2fed78f446338ecd589eeb505ae9f45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 15 Jul 2020 11:31:38 +0200 Subject: [PATCH 03/23] extract: extract_one: make arguments const --- extract.c | 4 ++-- extract.h | 4 ++-- terminal.h | 3 +++ 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/extract.c b/extract.c index b119bc3f..663f94ef 100644 --- a/extract.c +++ b/extract.c @@ -84,8 +84,8 @@ ensure_size(struct extraction_context *ctx, size_t additional_chars) } bool -extract_one(struct terminal *term, struct row *row, struct cell *cell, - int col, void *context) +extract_one(const struct terminal *term, const struct row *row, + const struct cell *cell, int col, void *context) { struct extraction_context *ctx = context; diff --git a/extract.h b/extract.h index 2219780d..0b3693d2 100644 --- a/extract.h +++ b/extract.h @@ -10,8 +10,8 @@ struct extraction_context; struct extraction_context *extract_begin(enum selection_kind kind); bool extract_one( - struct terminal *term, struct row *row, struct cell *cell, int col, - void *context); + 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/terminal.h b/terminal.h index 4d5f717b..fece4b50 100644 --- a/terminal.h +++ b/terminal.h @@ -542,3 +542,6 @@ 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, wchar_t **text); +bool term_view_to_text(const struct terminal *term, wchar_t **text); From ffaa19ee22ec00da01d23748f78253ad5c2253de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 15 Jul 2020 11:31:57 +0200 Subject: [PATCH 04/23] selection: provide a const-wrapper for extract_one() extract_one() takes const pointers, while the callback argument to foreach() expects non-const. --- selection.c | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/selection.c b/selection.c index eb21d318..d5add464 100644 --- a/selection.c +++ b/selection.c @@ -195,6 +195,14 @@ foreach_selected( assert(false); } +static bool +extract_one_const_wrapper(struct terminal *term, + struct row *row, struct cell *cell, + int col, void *data) +{ + return extract_one(term, row, cell, col, data); +} + static char * extract_selection(const struct terminal *term) { @@ -204,7 +212,7 @@ extract_selection(const struct terminal *term) foreach_selected( (struct terminal *)term, term->selection.start, term->selection.end, - &extract_one, ctx); + &extract_one_const_wrapper, ctx); char *text; return extract_finish(ctx, &text, NULL) ? text : NULL; From e6acafa11871af8992d5935f3f60d18c79e791b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 15 Jul 2020 11:32:40 +0200 Subject: [PATCH 05/23] extract: extract_one() sets a fail flag that extract_finish() reads This allows us to safely call extract_finish() when extract_one() failed, and we'll get the expected result; false, indicating extract_finish() failed. --- extract.c | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/extract.c b/extract.c index 663f94ef..49cd198a 100644 --- a/extract.c +++ b/extract.c @@ -10,6 +10,7 @@ struct extraction_context { 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; @@ -28,9 +29,18 @@ extract_begin(enum selection_kind kind) 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 */ ctx->buf[ctx->idx] = L'\0'; @@ -43,8 +53,6 @@ extract_finish(struct extraction_context *ctx, char **text, size_t *len) ctx->buf[ctx->idx] = L'\0'; } - bool ret = false; - size_t _len = wcstombs(NULL, ctx->buf, 0); if (_len == (size_t)-1) { LOG_ERRNO("failed to convert selection to UTF-8"); @@ -105,7 +113,7 @@ extract_one(const struct terminal *term, const struct row *row, /* Row has a hard linebreak, or either last cell or * current cell is empty */ if (!ensure_size(ctx, 1)) - return false; + goto err; ctx->buf[ctx->idx++] = L'\n'; ctx->empty_count = 0; @@ -115,7 +123,7 @@ extract_one(const struct terminal *term, const struct row *row, else if (ctx->selection_kind == SELECTION_BLOCK) { /* Always insert a linebreak */ if (!ensure_size(ctx, 1)) - return false; + goto err; ctx->buf[ctx->idx++] = L'\n'; ctx->empty_count = 0; @@ -131,7 +139,7 @@ extract_one(const struct terminal *term, const struct row *row, /* Replace empty cells with spaces when followed by non-empty cell */ if (!ensure_size(ctx, ctx->empty_count)) - return false; + goto err; for (size_t i = 0; i < ctx->empty_count; i++) ctx->buf[ctx->idx++] = L' '; @@ -144,7 +152,7 @@ extract_one(const struct terminal *term, const struct row *row, = &term->composed[cell->wc - CELL_COMB_CHARS_LO]; if (!ensure_size(ctx, 1 + composed->count)) - return false; + goto err; ctx->buf[ctx->idx++] = composed->base; for (size_t i = 0; i < composed->count; i++) @@ -153,11 +161,15 @@ extract_one(const struct terminal *term, const struct row *row, else { if (!ensure_size(ctx, 1)) - return false; + goto err; ctx->buf[ctx->idx++] = cell->wc; } ctx->last_row = row; ctx->last_cell = cell; return true; + +err: + ctx->failed = true; + return false; } From 4d17423ed184ad0f0343ca62d0b101a7a042941b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 15 Jul 2020 11:33:37 +0200 Subject: [PATCH 06/23] term: add term_scrollback_to_text() and term_view_to_text() These functions extract the current view, or the entire scrollback as an UTF-8 encoded byte buffer. --- terminal.c | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ terminal.h | 6 ++++-- 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/terminal.c b/terminal.c index 40bfbc90..d237de97 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" @@ -2472,3 +2473,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 fece4b50..40f64387 100644 --- a/terminal.h +++ b/terminal.h @@ -543,5 +543,7 @@ 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, wchar_t **text); -bool term_view_to_text(const struct terminal *term, wchar_t **text); +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); From 9cdccdd2ac5140a5a265926501a919a3b86f8d84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 15 Jul 2020 12:39:10 +0200 Subject: [PATCH 07/23] term: break out fork+exec functionality to a separate file --- meson.build | 1 + spawn.c | 72 +++++++++++++++++++++++++++++++++++++++++++++++++++++ spawn.h | 6 +++++ terminal.c | 56 +++-------------------------------------- 4 files changed, 82 insertions(+), 53 deletions(-) create mode 100644 spawn.c create mode 100644 spawn.h diff --git a/meson.build b/meson.build index a64f239f..a7db2105 100644 --- a/meson.build +++ b/meson.build @@ -123,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/spawn.c b/spawn.c new file mode 100644 index 00000000..94bbb928 --- /dev/null +++ b/spawn.c @@ -0,0 +1,72 @@ +#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 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 ((cwd != NULL && chdir(cwd) < 0) || + //execlp(term->foot_exe, term->foot_exe, NULL) < 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..129cadc2 --- /dev/null +++ b/spawn.h @@ -0,0 +1,6 @@ +#pragma once + +#include +#include "reaper.h" + +bool spawn(struct reaper *reaper, const char *cwd, char *const argv[]); diff --git a/terminal.c b/terminal.c index d237de97..53640baa 100644 --- a/terminal.c +++ b/terminal.c @@ -30,6 +30,7 @@ #include "selection.h" #include "sixel.h" #include "slave.h" +#include "spawn.h" #include "util.h" #include "vt.h" @@ -2246,59 +2247,8 @@ 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}); } void From f21ea97037bd11f2375fff5f82dc2d08d0d552b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 15 Jul 2020 13:26:13 +0200 Subject: [PATCH 08/23] input: parse_key_binding: free key-codes list if 'bindings' is NULL --- input.c | 3 ++- wayland.h | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/input.c b/input.c index 2df8c73a..af0619ac 100644 --- a/input.c +++ b/input.c @@ -197,7 +197,8 @@ input_parse_key_binding(struct xkb_keymap *keymap, const char *combos, }; tll_push_back(*bindings, binding); - } + } else + tll_free(key_codes); } free(copy); diff --git a/wayland.h b/wayland.h index c47d3c29..9da2c856 100644 --- a/wayland.h +++ b/wayland.h @@ -47,6 +47,7 @@ enum bind_action_normal { struct key_binding_normal { struct key_binding bind; enum bind_action_normal action; + const char *spawn; }; struct mouse_binding { From 57f5cc1bf2d98e6a49f74b39a1b0491b8bb65e51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 15 Jul 2020 13:32:31 +0200 Subject: [PATCH 09/23] config: add support for appending a 'spawn' argument to key bindings A key binding may now have an optional ':' string appended to the key. This is intended to be used like so: pipe-scrollback:sh -c "cat > file"=Print TODO: we still only allow one *action*. Meaning you still cannot specify multiple pipe-scrollback bindings, for example. --- config.c | 19 ++++++++++++++++--- config.h | 2 ++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/config.c b/config.c index f8be377d..f5820e98 100644 --- a/config.c +++ b/config.c @@ -530,11 +530,20 @@ 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 *spawn = strchr(key, ':'); + if (spawn != NULL) + spawn++; + + const size_t key_len = spawn != NULL ? spawn - key - 1: strlen(key); + + for (enum bind_action_normal action = 0; + action < BIND_ACTION_COUNT; + action++) + { if (binding_action_map[action] == NULL) continue; - if (strcmp(key, binding_action_map[action]) != 0) + if (strncmp(key, binding_action_map[action], key_len) != 0) continue; if (strcasecmp(value, "none") == 0) { @@ -548,7 +557,9 @@ parse_section_key_bindings( } free(conf->bindings.key[action]); + free(conf->bindings.spawn[action]); conf->bindings.key[action] = strdup(value); + conf->bindings.spawn[action] = spawn != NULL ? strdup(spawn) : NULL; return true; } @@ -984,8 +995,10 @@ 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++) + for (enum bind_action_normal i = 0; i < BIND_ACTION_COUNT; i++) { free(conf.bindings.key[i]); + free(conf.bindings.spawn[i]); + } for (enum bind_action_search i = 0; i < BIND_ACTION_SEARCH_COUNT; i++) free(conf.bindings.search[i]); } diff --git a/config.h b/config.h index caf269b5..06ee197c 100644 --- a/config.h +++ b/config.h @@ -49,6 +49,8 @@ struct config { struct { /* Bindings for "normal" mode */ char *key[BIND_ACTION_COUNT]; + char *spawn[BIND_ACTION_COUNT]; + struct mouse_binding mouse[BIND_ACTION_COUNT]; /* From 69d9ff3f2573dc46d558a5134a93165a7ec212d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 15 Jul 2020 13:33:56 +0200 Subject: [PATCH 10/23] spawn: add optional stdin/stdout/stderr redirection FDs If not -1, spawn() will redirect the child's stdin/stdout/stderr to these FDs. --- spawn.c | 10 +++++++--- spawn.h | 3 ++- terminal.c | 3 ++- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/spawn.c b/spawn.c index 94bbb928..b268bfe5 100644 --- a/spawn.c +++ b/spawn.c @@ -13,7 +13,8 @@ #include "log.h" bool -spawn(struct reaper *reaper, const char *cwd, char *const argv[]) +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) { @@ -30,8 +31,11 @@ spawn(struct reaper *reaper, const char *cwd, char *const argv[]) if (pid == 0) { /* Child */ close(pipe_fds[0]); - if ((cwd != NULL && chdir(cwd) < 0) || - //execlp(term->foot_exe, term->foot_exe, NULL) < 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)); diff --git a/spawn.h b/spawn.h index 129cadc2..2ab645a8 100644 --- a/spawn.h +++ b/spawn.h @@ -3,4 +3,5 @@ #include #include "reaper.h" -bool spawn(struct reaper *reaper, const char *cwd, char *const argv[]); +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 53640baa..c720aea4 100644 --- a/terminal.c +++ b/terminal.c @@ -2248,7 +2248,8 @@ bool term_spawn_new(const struct terminal *term) { return spawn( - term->reaper, term->cwd, (char *const []){term->foot_exe, NULL}); + term->reaper, term->cwd, (char *const []){term->foot_exe, NULL}, + -1, -1, -1); } void From 062b0eb7ab69e7821ab79763bfdc3648fdbb5f40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 15 Jul 2020 13:34:32 +0200 Subject: [PATCH 11/23] input: wip: initial support for piping scrollback/view to external tools --- input.c | 139 +++++++++++++++++++++++++++++++++++++++++++++++++++--- wayland.h | 2 +- 2 files changed, 133 insertions(+), 8 deletions(-) diff --git a/input.c b/input.c index af0619ac..00382c4f 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: @@ -102,10 +145,89 @@ execute_binding(struct seat *seat, struct terminal *term, break; case BIND_ACTION_PIPE_SCROLLBACK: - case BIND_ACTION_PIPE_VIEW: - LOG_ERR("unimplemented"); + 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; + } + + 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() */ + { + 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], -1, -1)) + 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, + }; + + if (!fdm_add(term->fdm, pipe_fd[1], EPOLLOUT, &fdm_write_pipe, ctx)) + goto pipe_err; + break; + pipe_err: + 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; @@ -284,7 +406,10 @@ keyboard_keymap(void *data, struct wl_keyboard *wl_keyboard, tll_foreach(bindings, it) { tll_push_back( seat->kbd.bindings.key, - ((struct key_binding_normal){.bind = it->item, .action = i})); + ((struct key_binding_normal){ + .bind = it->item, + .action = i, + .pipe_cmd = wayl->conf->bindings.spawn[i]})); } tll_free(bindings); @@ -602,14 +727,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; } } @@ -1259,7 +1384,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/wayland.h b/wayland.h index 9da2c856..b11af0d3 100644 --- a/wayland.h +++ b/wayland.h @@ -47,7 +47,7 @@ enum bind_action_normal { struct key_binding_normal { struct key_binding bind; enum bind_action_normal action; - const char *spawn; + const char *pipe_cmd; }; struct mouse_binding { From 1541531765f517067dcb92b4870d64e1be9bd230 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 15 Jul 2020 16:39:07 +0200 Subject: [PATCH 12/23] conf: bindings: allow several mappings for the same action If the bindings has different pipe command associated with them, treat them as different actions. --- config.c | 203 ++++++++++++++++++++++++++++++++++++------------------ config.h | 20 ++++-- input.c | 28 ++++---- wayland.h | 2 +- 4 files changed, 166 insertions(+), 87 deletions(-) diff --git a/config.c b/config.c index f5820e98..044266f1 100644 --- a/config.c +++ b/config.c @@ -485,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; } @@ -530,11 +528,11 @@ parse_section_key_bindings( const char *key, const char *value, struct config *conf, const char *path, unsigned lineno) { - const char *spawn = strchr(key, ':'); - if (spawn != NULL) - spawn++; + const char *pipe_cmd = strchr(key, ':'); + if (pipe_cmd != NULL) + pipe_cmd++; - const size_t key_len = spawn != NULL ? spawn - key - 1: strlen(key); + const size_t key_len = pipe_cmd != NULL ? pipe_cmd - key - 1: strlen(key); for (enum bind_action_normal action = 0; action < BIND_ACTION_COUNT; @@ -547,19 +545,46 @@ 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]); - free(conf->bindings.spawn[action]); - conf->bindings.key[action] = strdup(value); - conf->bindings.spawn[action] = spawn != NULL ? strdup(spawn) : NULL; + 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 && + strcmp(it->item.pipe_cmd, pipe_cmd) == 0))) + { + + free(it->item.key); + free(it->item.pipe_cmd); + + it->item.key = strdup(value); + it->item.pipe_cmd = pipe_cmd != NULL ? strdup(pipe_cmd) : NULL; + already_added = true; + break; + } + } + + if (!already_added) { + struct config_key_binding_normal binding = { + .action = action, + .key = strdup(value), + .pipe_cmd = pipe_cmd != NULL ? strdup(pipe_cmd) : NULL, + }; + tll_push_back(conf->bindings.key, binding); + } return true; } @@ -581,7 +606,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; } @@ -603,16 +633,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; } @@ -896,42 +942,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, @@ -951,6 +961,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) { @@ -995,12 +1062,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]); - free(conf.bindings.spawn[i]); + tll_foreach(conf.bindings.key, it) { + free(it->item.key); + free(it->item.pipe_cmd); } - for (enum bind_action_search i = 0; i < BIND_ACTION_SEARCH_COUNT; i++) - free(conf.bindings.search[i]); + 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 06ee197c..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,10 +60,8 @@ struct config { struct { /* Bindings for "normal" mode */ - char *key[BIND_ACTION_COUNT]; - char *spawn[BIND_ACTION_COUNT]; - - struct mouse_binding mouse[BIND_ACTION_COUNT]; + tll(struct config_key_binding_normal) key; + tll(struct mouse_binding) mouse; /* * Special modes @@ -59,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/input.c b/input.c index 00382c4f..e9978fb2 100644 --- a/input.c +++ b/input.c @@ -398,34 +398,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, - .pipe_cmd = wayl->conf->bindings.spawn[i]})); + .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); } } @@ -1370,9 +1369,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 */ diff --git a/wayland.h b/wayland.h index b11af0d3..dbf63c1b 100644 --- a/wayland.h +++ b/wayland.h @@ -51,9 +51,9 @@ struct key_binding_normal { }; struct mouse_binding { + enum bind_action_normal action; uint32_t button; int count; - enum bind_action_normal action; }; enum bind_action_search { From be9c566622959d2435caaab0004e498a79d683ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 15 Jul 2020 16:47:01 +0200 Subject: [PATCH 13/23] input: comments --- input.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/input.c b/input.c index e9978fb2..ff4f9edf 100644 --- a/input.c +++ b/input.c @@ -184,7 +184,8 @@ execute_binding(struct seat *seat, struct terminal *term, } } - /* Make sure write-end is closed on exec() */ + /* 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 || @@ -211,6 +212,7 @@ execute_binding(struct seat *seat, struct terminal *term, .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; From 65a46be822fe290ae721d1dd668f54dcc7a7ec49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 15 Jul 2020 17:28:09 +0200 Subject: [PATCH 14/23] config: change syntax for pipe-command in key binding The pipe command can contain almost all printable characters. In particular, we can expect it to contain '='. Having the pipe command as part of the key breaks the key/value splitting. Change it so that it is instead an optional initial part of the value, enclosed in '[]'. I.e. instead of pipe-visible:cmd=binding we now need to write pipe-visible=[cmd] binding --- config.c | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/config.c b/config.c index 044266f1..4ed3e2f4 100644 --- a/config.c +++ b/config.c @@ -528,11 +528,21 @@ parse_section_key_bindings( const char *key, const char *value, struct config *conf, const char *path, unsigned lineno) { - const char *pipe_cmd = strchr(key, ':'); - if (pipe_cmd != NULL) - pipe_cmd++; + const char *pipe_cmd = NULL; + size_t pipe_len = 0; - const size_t key_len = pipe_cmd != NULL ? pipe_cmd - key - 1: strlen(key); + 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; @@ -541,7 +551,7 @@ parse_section_key_bindings( if (binding_action_map[action] == NULL) continue; - if (strncmp(key, binding_action_map[action], key_len) != 0) + if (strcmp(key, binding_action_map[action]) != 0) continue; if (strcasecmp(value, "none") == 0) { @@ -564,14 +574,15 @@ parse_section_key_bindings( if (it->item.action == action && ((it->item.pipe_cmd == NULL && pipe_cmd == NULL) || (it->item.pipe_cmd != NULL && pipe_cmd != NULL && - strcmp(it->item.pipe_cmd, pipe_cmd) == 0))) + 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 ? strdup(pipe_cmd) : NULL; + it->item.pipe_cmd = pipe_cmd != NULL + ? strndup(pipe_cmd, pipe_len) : NULL; already_added = true; break; } From 94bb31328279a8ca0c598d08bb62e5831e601264 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 15 Jul 2020 17:48:02 +0200 Subject: [PATCH 15/23] config: oopsie, need to use strndup() on the pipe command Since it isn't NULL-terminated. --- config.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.c b/config.c index 4ed3e2f4..283a72ea 100644 --- a/config.c +++ b/config.c @@ -592,7 +592,7 @@ parse_section_key_bindings( struct config_key_binding_normal binding = { .action = action, .key = strdup(value), - .pipe_cmd = pipe_cmd != NULL ? strdup(pipe_cmd) : NULL, + .pipe_cmd = pipe_cmd != NULL ? strndup(pipe_cmd, pipe_len) : NULL, }; tll_push_back(conf->bindings.key, binding); } From 48fa484f1838d10067e355f57faa3334657d0e0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 15 Jul 2020 17:48:37 +0200 Subject: [PATCH 16/23] doc: foot.5: document pipe-visible and pipe-scrollback --- doc/foot.5.scd | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/doc/foot.5.scd b/doc/foot.5.scd index 747c9cb3..2673bfef 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 | fuzzel --dmenu | xargs -r firefox"] Control+Print* + + Default: _not bound_ + # SECTION: mouse-bindings This section lets you override the default mouse bindings. From 6ba6858fdc36da462eb4c133019d0eef2c72164a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 15 Jul 2020 17:53:36 +0200 Subject: [PATCH 17/23] changelog: document pipe-visible and pipe-scrollback bindings --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fb27893..e901e1dd 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 | fuzzel --dmenu | xargs -r firefox] Control+Print` ### Changed From e04b8f672b142fd3fb447e0bf406b10b0a632b9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 15 Jul 2020 17:54:25 +0200 Subject: [PATCH 18/23] changelog: minor --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e901e1dd..d42f3de8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,7 +27,7 @@ * **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 | fuzzel --dmenu | xargs -r firefox] Control+Print` + `pipe-visible=[sh -c "xurls | fuzzel --dmenu | xargs -r firefox] Control+Print` ### Changed From 8242ff5958a1d96d8da96ab92f2e51e365a5642b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 15 Jul 2020 18:02:08 +0200 Subject: [PATCH 19/23] doc: foot.5: change pipe-visible example to use bemenu instead of fuzzel --- doc/foot.5.scd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/foot.5.scd b/doc/foot.5.scd index 2673bfef..92ac7c01 100644 --- a/doc/foot.5.scd +++ b/doc/foot.5.scd @@ -247,7 +247,7 @@ e.g. *search-start=none*. use *sh -c "command line"* if you need that. Example: - *pipe-visible=[sh -c "xurls | fuzzel --dmenu | xargs -r firefox"] Control+Print* + *pipe-visible=[sh -c "xurls | bemenu | xargs -r firefox"] Control+Print* Default: _not bound_ From 765b714642cb14b461b1e005290f25066c57c983 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 15 Jul 2020 18:02:25 +0200 Subject: [PATCH 20/23] footrc: add pipe-visible and pipe-scrollback --- footrc | 2 ++ 1 file changed, 2 insertions(+) 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 From 22c73eaf4ba37ff8a7982de893e3546cd209aa79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 15 Jul 2020 18:17:28 +0200 Subject: [PATCH 21/23] input: pipe-{visible,scrollback}: redirect stdout/stderr to /dev/null --- input.c | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/input.c b/input.c index ff4f9edf..0e4cb675 100644 --- a/input.c +++ b/input.c @@ -163,6 +163,14 @@ execute_binding(struct seat *seat, struct terminal *term, 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; @@ -196,7 +204,7 @@ execute_binding(struct seat *seat, struct terminal *term, } } - if (!spawn(term->reaper, NULL, argv, pipe_fd[0], -1, -1)) + if (!spawn(term->reaper, NULL, argv, pipe_fd[0], stdout_fd, stderr_fd)) goto pipe_err; /* Not needed anymore */ @@ -219,6 +227,10 @@ execute_binding(struct seat *seat, struct terminal *term, 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) From ad479cae0539fa50051b361b2299af33aadc51bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 15 Jul 2020 18:22:40 +0200 Subject: [PATCH 22/23] changelog: use bemenu instead of fuzzel in pipe-visible example --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d42f3de8..a7f27bc1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,7 +27,7 @@ * **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 | fuzzel --dmenu | xargs -r firefox] Control+Print` + `pipe-visible=[sh -c "xurls | bemenu | xargs -r firefox] Control+Print` ### Changed From e5401c845c058d53aff9586cf12e0074e74059aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 16 Jul 2020 17:46:02 +0200 Subject: [PATCH 23/23] extract: finish: allocate buffer before writing the terminator When all cells were empty, we'll have written 0 bytes to the buffer and hence it will still be un-allocated (i.e. NULL). --- extract.c | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/extract.c b/extract.c index 49cd198a..b626d3e6 100644 --- a/extract.c +++ b/extract.c @@ -26,6 +26,23 @@ extract_begin(enum 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) { @@ -43,6 +60,8 @@ extract_finish(struct extraction_context *ctx, char **text, size_t *len) 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); @@ -73,24 +92,6 @@ out: return ret; } -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_one(const struct terminal *term, const struct row *row, const struct cell *cell, int col, void *context)