feat: ansi for pipe rows

This commit is contained in:
saeedark 2025-10-08 02:38:45 +03:30 committed by saeedark
parent 5587604469
commit 312b22300d
10 changed files with 566 additions and 23 deletions

View file

@ -132,6 +132,7 @@ static const char *const binding_action_map[] = {
[BIND_ACTION_PIPE_VIEW] = "pipe-visible",
[BIND_ACTION_PIPE_SELECTED] = "pipe-selected",
[BIND_ACTION_PIPE_COMMAND_OUTPUT] = "pipe-command-output",
[BIND_ACTION_TOGGLE_ANSI_SELECTION] = "toggle-ansi-selection",
[BIND_ACTION_SHOW_URLS_COPY] = "show-urls-copy",
[BIND_ACTION_SHOW_URLS_LAUNCH] = "show-urls-launch",
[BIND_ACTION_SHOW_URLS_PERSISTENT] = "show-urls-persistent",
@ -923,6 +924,9 @@ parse_section_main(struct context *ctx)
else if (streq(key, "app-id"))
return value_to_str(ctx, &conf->app_id);
else if (streq(key, "ansi-pipe"))
return value_to_bool(ctx, &conf->ansi_pipe);
else if (streq(key, "initial-window-size-pixels")) {
if (!value_to_dimensions(ctx, &conf->size.width, &conf->size.height))
return false;

View file

@ -416,6 +416,8 @@ struct config {
char *utmp_helper_path;
bool ansi_pipe;
struct {
enum fcft_scaling_filter fcft_filter;
bool overflowing_glyphs;

552
extract.c
View file

@ -1,4 +1,5 @@
#include "extract.h"
#include "terminal.h"
#include <string.h>
#define LOG_MODULE "extract"
@ -18,27 +19,122 @@ struct extraction_context {
const struct row *last_row;
const struct cell *last_cell;
enum selection_kind selection_kind;
bool rich;
bool bold; // 0
bool dim; // 1
bool italic; // 2
bool underline; // 3
bool blink; // 4
bool reverse; // 5
bool conceal; // 6
bool strikethrough; // 7
uint32_t fg; // 8
uint32_t bg; // 9
uint32_t un; // 3
enum color_source fg_src; // 8
enum color_source bg_src; // 9
enum color_source un_src; // 3
enum underline_style underline_style; // 3
uint64_t url_id; // 10
};
struct extraction_context *
extract_begin(enum selection_kind kind, bool strip_trailing_empty)
{
struct extraction_context *ctx = malloc(sizeof(*ctx));
if (unlikely(ctx == NULL)) {
LOG_ERRNO("malloc() failed");
return NULL;
uint16_t
compare_attrs(struct extraction_context *ctx, struct attributes attrs, const struct row *row, int col) {
uint16_t diff = 0;
uint8_t idx;
const struct row_data *extra = row->extra;
idx = 0;
if (ctx->bold != attrs.bold)
diff |= 1 << idx;
idx = 1;
if (ctx->dim != attrs.dim)
diff |= 1 << idx;
idx = 2;
if (ctx->italic != attrs.italic)
diff |= 1 << idx;
idx = 3;
if (ctx->underline != attrs.underline)
diff |= 1 << idx;
if (extra != NULL){
for (size_t i = 0; i < extra->underline_ranges.count; i++){
const struct row_range *range = &extra->underline_ranges.v[i];
if (range->start <= col && col <= range->end){
if (ctx->underline_style != range->underline.style){
diff |= 1 << idx;
break;
}
if (ctx->un_src != range->underline.color_src){
diff |= 1 << idx;
break;
}
if ((range->underline.color_src != COLOR_DEFAULT) && ctx->un != range->underline.color){
diff |= 1 << idx;
break;
}
break;
}
}
}
*ctx = (struct extraction_context){
.selection_kind = kind,
.strip_trailing_empty = strip_trailing_empty,
};
return ctx;
idx = 4;
if (ctx->blink != attrs.blink)
diff |= 1 << idx;
idx = 5;
if (ctx->reverse != attrs.reverse)
diff |= 1 << idx;
idx = 6;
if (ctx->conceal != attrs.conceal)
diff |= 1 << idx;
idx = 7;
if (ctx->strikethrough != attrs.strikethrough)
diff |= 1 << idx;
idx = 8;
if (ctx->fg_src != attrs.fg_src)
diff |= 1 << idx;
if ((attrs.fg_src != COLOR_DEFAULT) && ctx->fg != attrs.fg)
diff |= 1 << idx;
idx = 9;
if (ctx->bg_src != attrs.bg_src)
diff |= 1 << idx;
if ((attrs.bg_src != COLOR_DEFAULT) && ctx->bg != attrs.bg)
diff |= 1 << idx;
idx = 10;
if (extra != NULL){
bool found_one = false;
for (size_t i = 0; i < extra->uri_ranges.count; i++){
const struct row_range *range = &extra->uri_ranges.v[i];
if (range->start <= col && col <= range->end){
found_one = true;
if (ctx->url_id != range->uri.id)
diff |= 1 << idx;
break;
}
}
if (!found_one && ctx->url_id)
diff |= 1 << idx;
} else if (ctx->url_id) {
diff |= 1 << idx;
}
return diff;
}
static bool
ensure_size(struct extraction_context *ctx, size_t additional_chars)
{
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;
char32_t *new_buf = realloc(ctx->buf, new_size * sizeof(new_buf[0]));
@ -54,6 +150,413 @@ ensure_size(struct extraction_context *ctx, size_t additional_chars)
return true;
}
bool
clear_rich_ctx(struct extraction_context *ctx) {
if (ctx->url_id){
if (!ensure_size(ctx, 7))
return false;
ctx->buf[ctx->idx++] = U'\x1b';
ctx->buf[ctx->idx++] = U']';
ctx->buf[ctx->idx++] = U'8';
ctx->buf[ctx->idx++] = U';';
ctx->buf[ctx->idx++] = U';';
ctx->buf[ctx->idx++] = U'\x1b';
ctx->buf[ctx->idx++] = U'\\';
}
if (ctx->bold +
ctx->dim +
ctx->italic +
ctx->underline +
ctx->blink +
ctx->reverse +
ctx->conceal +
ctx->strikethrough +
ctx->fg_src +
ctx->bg_src +
ctx->un_src +
ctx->underline_style
)
{
if (!ensure_size(ctx, 4))
return false;
ctx->buf[ctx->idx++] = U'\x1b';
ctx->buf[ctx->idx++] = U'[';
ctx->buf[ctx->idx++] = U'0';
ctx->buf[ctx->idx++] = U'm';
}
ctx->bold = false;
ctx->dim = false;
ctx->italic = false;
ctx->underline = false;
ctx->blink = false;
ctx->reverse = false;
ctx->conceal = false;
ctx->strikethrough = false;
ctx->fg = 0;
ctx->bg = 0;
ctx->un = 0;
ctx->fg_src = 0;
ctx->bg_src = 0;
ctx->un_src = 0;
ctx->underline_style = 0;
ctx->url_id = 0;
return true;
}
bool
init_x1b(bool *x1b, struct extraction_context *ctx) {
if (!(*x1b)) {
if (!ensure_size(ctx, 2))
return false;
ctx->buf[ctx->idx++] = U'\x1b';
ctx->buf[ctx->idx++] = U'[';
} else {
if (!ensure_size(ctx, 1))
return false;
ctx->buf[ctx->idx++] = U';';
}
*x1b = true;
return true;
}
bool
change_color_rich(enum color_source colour_src, uint32_t colour, struct extraction_context *ctx, uint8_t domain)
{
switch (colour_src) {
case COLOR_DEFAULT:
if (!ensure_size(ctx, 2))
return false;
ctx->buf[ctx->idx++] = U'0' + domain;
ctx->buf[ctx->idx++] = U'9';
break;
case COLOR_BASE16:
xassert(domain != 0);
if (!ensure_size(ctx, 2 + (((domain + (6 * (colour > 7)))) > 9)))
return false;
if (((domain + (6 * (colour > 7)))) > 9)
ctx->buf[ctx->idx++] = U'1';
ctx->buf[ctx->idx++] = U'0' + domain + (6 * (colour > 7)) - 10 * (((domain + (6 * (colour > 7)))) > 9);
ctx->buf[ctx->idx++] = U'0' + colour - (8 * (colour > 7));
break;
case COLOR_BASE256:
if (!ensure_size(ctx, 6 + (colour > 9) + (colour > 99)))
return false;
ctx->buf[ctx->idx++] = U'0' + domain;
ctx->buf[ctx->idx++] = U'8';
ctx->buf[ctx->idx++] = U';';
ctx->buf[ctx->idx++] = U'5';
ctx->buf[ctx->idx++] = U';';
if (colour > 99)
ctx->buf[ctx->idx++] = U'0' + (colour / 100);
if (colour > 9)
ctx->buf[ctx->idx++] = U'0' + ((colour % 100) / 10);
ctx->buf[ctx->idx++] = U'0' + (colour % 10);
break;
case COLOR_RGB:;
uint8_t r = (colour >> 16) & 0xff;
uint8_t g = (colour >> 8) & 0xff;
uint8_t b = colour & 0xff;
if (!ensure_size(ctx, 8 + (r > 9) + (r > 99) + (g > 9) + (g > 99) + (b > 9) + (b > 99)))
return false;
ctx->buf[ctx->idx++] = U'0' + domain;
ctx->buf[ctx->idx++] = U'8';
ctx->buf[ctx->idx++] = U';';
ctx->buf[ctx->idx++] = U'2';
ctx->buf[ctx->idx++] = U';';
if (r > 99)
ctx->buf[ctx->idx++] = U'0' + (r / 100);
if (r > 9)
ctx->buf[ctx->idx++] = U'0' + ((r % 100) / 10);
ctx->buf[ctx->idx++] = U'0' + (r % 10);
ctx->buf[ctx->idx++] = U';';
if (g > 99)
ctx->buf[ctx->idx++] = U'0' + (g / 100);
if (g > 9)
ctx->buf[ctx->idx++] = U'0' + ((g % 100) / 10);
ctx->buf[ctx->idx++] = U'0' + (g % 10);
ctx->buf[ctx->idx++] = U';';
if (b > 99)
ctx->buf[ctx->idx++] = U'0' + (b / 100);
if (b > 9)
ctx->buf[ctx->idx++] = U'0' + ((b % 100) / 10);
ctx->buf[ctx->idx++] = U'0' + (b % 10);
break;
}
return true;
}
bool
style_flip_rich(bool attr, uint8_t attr_idx, struct extraction_context *ctx) {
if (attr) {
if (!ensure_size(ctx, 1))
return false;
ctx->buf[ctx->idx++] = U'0' + attr_idx;
} else {
if (!ensure_size(ctx, 2))
return false;
ctx->buf[ctx->idx++] = U'2';
ctx->buf[ctx->idx++] = U'0' + attr_idx;
}
return true;
}
bool
add_rich_diff(struct extraction_context *ctx, struct attributes attrs, const struct row *row, int col, uint16_t diff) {
char idx;
bool x1b = false;
/* dim and bod */
idx = 0;
if (diff & 1 << idx || diff & 1 << (idx + 1)) {
x1b = true;
if (!ensure_size(ctx, 2))
goto err;
ctx->buf[ctx->idx++] = U'\x1b';
ctx->buf[ctx->idx++] = U'[';
if ((!attrs.bold && !attrs.dim) || (attrs.bold ^ attrs.dim)) {
if (!ensure_size(ctx, 2 + (2 * (attrs.bold ^ attrs.dim))))
goto err;
ctx->buf[ctx->idx++] = U'2';
ctx->buf[ctx->idx++] = U'2';
if (attrs.bold ^ attrs.dim) {
ctx->buf[ctx->idx++] = U';';
if (attrs.dim) {
ctx->buf[ctx->idx++] = U'2';
}
else if (attrs.bold) {
ctx->buf[ctx->idx++] = U'1';
}
}
}
ctx->dim = attrs.dim;
ctx->bold = attrs.bold;
}
/* italic */
idx = 2;
if (diff & 1 << idx) {
if (!init_x1b(&x1b, ctx))
goto err;
if (!style_flip_rich(attrs.italic, 3, ctx))
goto err;
ctx->italic = attrs.italic;
}
/* underline */
idx = 3;
if (diff & 1 << idx) {
if (attrs.underline) {
const struct row_data *extra = row->extra;
if (extra != NULL) {
for (size_t i = 0; i < extra->underline_ranges.count; i++) {
const struct row_range *range = &extra->underline_ranges.v[i];
if (range->start <= col && col <= range->end) {
if (ctx->underline_style != range->underline.style) {
if (!init_x1b(&x1b, ctx))
goto err;
if (!ensure_size(ctx, 3))
goto err;
ctx->buf[ctx->idx++] = U'4';
ctx->buf[ctx->idx++] = U':';
ctx->buf[ctx->idx++] = U'0' + range->underline.style;
}
if ((ctx->un_src != range->underline.color_src) || ((range->underline.color_src != COLOR_DEFAULT) && ctx->un != range->underline.color)) {
if (!init_x1b(&x1b, ctx))
goto err;
if (!change_color_rich(range->underline.color_src, range->underline.color, ctx, 5))
goto err;
}
ctx->underline_style = range->underline.style;
ctx->un = range->underline.color;
ctx->un_src = range->underline.color_src;
break;
}
}
} else {
if (!init_x1b(&x1b, ctx))
goto err;
if (!ensure_size(ctx, 3))
goto err;
ctx->buf[ctx->idx++] = U'4';
ctx->buf[ctx->idx++] = U':';
ctx->buf[ctx->idx++] = U'1';
}
} else {
if (!init_x1b(&x1b, ctx))
goto err;
if (!ensure_size(ctx, 3))
goto err;
ctx->buf[ctx->idx++] = U'4';
ctx->buf[ctx->idx++] = U':';
ctx->buf[ctx->idx++] = U'0';
}
ctx->underline = attrs.underline;
}
/* blink */
idx = 4;
if (diff & 1 << idx) {
if (!init_x1b(&x1b, ctx))
goto err;
if (!style_flip_rich(attrs.blink, 5, ctx))
goto err;
ctx->blink = attrs.blink;
}
/* reverse */
idx = 5;
if (diff & 1 << idx) {
if (!init_x1b(&x1b, ctx))
goto err;
if (!style_flip_rich(attrs.reverse, 7, ctx))
goto err;
ctx->reverse = attrs.reverse;
}
/* conceal */
idx = 6;
if (diff & 1 << idx) {
if (!init_x1b(&x1b, ctx))
goto err;
if (!style_flip_rich(attrs.conceal, 8, ctx))
goto err;
ctx->conceal = attrs.conceal;
}
/* strikethrough */
idx = 7;
if (diff & 1 << idx) {
if (!init_x1b(&x1b, ctx))
goto err;
if (!style_flip_rich(attrs.strikethrough, 9, ctx))
goto err;
ctx->strikethrough = attrs.strikethrough;
}
/* foreground colour */
idx = 8;
if (diff & 1 << idx) {
if (!init_x1b(&x1b, ctx))
goto err;
if (!change_color_rich(attrs.fg_src, attrs.fg, ctx, 3))
goto err;
ctx->fg = attrs.fg;
ctx->fg_src = attrs.fg_src;
}
/* background colour */
idx = 9;
if (diff & 1 << idx) {
if (!init_x1b(&x1b, ctx))
goto err;
if (!change_color_rich(attrs.bg_src, attrs.bg, ctx, 4))
goto err;
ctx->bg = attrs.bg;
ctx->bg_src = attrs.bg_src;
}
if (x1b) {
if (!ensure_size(ctx, 1))
goto err;
ctx->buf[ctx->idx++] = U'm';
}
idx = 10;
if (diff & 1 << idx) {
const struct row_data *extra = row->extra;
if (extra != NULL) {
char32_t *text;
bool found_one = false;
for (size_t i = 0; i < extra->uri_ranges.count; i++) {
const struct row_range *range = &extra->uri_ranges.v[i];
if (range->start <= col && col <= range->end) {
found_one = true;
ctx->url_id = range->uri.id;
text = ambstoc32(range->uri.uri);
if (!ensure_size(ctx, 7 + c32len(text)))
goto err;
ctx->buf[ctx->idx++] = U'\x1b';
ctx->buf[ctx->idx++] = U']';
ctx->buf[ctx->idx++] = U'8';
ctx->buf[ctx->idx++] = U';';
ctx->buf[ctx->idx++] = U';';
for (size_t j = 0; j < c32len(text); j++)
ctx->buf[ctx->idx++] = text[j];
ctx->buf[ctx->idx++] = U'\x1b';
ctx->buf[ctx->idx++] = U'\\';
free(text);
break;
}
}
if (!found_one) {
if (!ensure_size(ctx, 7))
goto err;
ctx->buf[ctx->idx++] = U'\x1b';
ctx->buf[ctx->idx++] = U']';
ctx->buf[ctx->idx++] = U'8';
ctx->buf[ctx->idx++] = U';';
ctx->buf[ctx->idx++] = U';';
ctx->buf[ctx->idx++] = U'\x1b';
ctx->buf[ctx->idx++] = U'\\';
ctx->url_id = 0;
}
} else {
if (!ensure_size(ctx, 7))
goto err;
ctx->buf[ctx->idx++] = U'\x1b';
ctx->buf[ctx->idx++] = U']';
ctx->buf[ctx->idx++] = U'8';
ctx->buf[ctx->idx++] = U';';
ctx->buf[ctx->idx++] = U';';
ctx->buf[ctx->idx++] = U'\x1b';
ctx->buf[ctx->idx++] = U'\\';
ctx->url_id = 0;
}
}
return true;
err:
free(ctx->buf);
free(ctx);
return false;
}
struct extraction_context *
extract_begin(enum selection_kind kind, bool strip_trailing_empty, bool rich) {
struct extraction_context *ctx = malloc(sizeof(*ctx));
if (unlikely(ctx == NULL)){
LOG_ERRNO("malloc() failed");
return NULL;
}
*ctx = (struct extraction_context){
.selection_kind = kind,
.strip_trailing_empty = strip_trailing_empty,
.rich = rich,
};
return ctx;
}
bool
extract_finish_wide(struct extraction_context *ctx, char32_t **text, size_t *len)
{
@ -111,6 +614,15 @@ extract_finish_wide(struct extraction_context *ctx, char32_t **text, size_t *len
}
}
if (ctx->rich){
ctx->idx = ctx->idx - 1;
if (!clear_rich_ctx(ctx))
goto err;
if (!ensure_size(ctx, 1))
goto err;
ctx->buf[ctx->idx++] = U'\0';
}
*text = ctx->buf;
if (len != NULL)
*len = ctx->idx - 1;
@ -157,12 +669,17 @@ extract_one(const struct terminal *term, const struct row *row,
const struct cell *cell, int col, void *context)
{
struct extraction_context *ctx = context;
struct attributes attrs = cell->attrs;
if (cell->wc >= CELL_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->rich){
if (!clear_rich_ctx(ctx))
goto err;
}
if (ctx->selection_kind != SELECTION_BLOCK) {
if (ctx->last_row->linebreak ||
@ -230,6 +747,13 @@ extract_one(const struct terminal *term, const struct row *row,
ctx->newline_count = 0;
ctx->empty_count = 0;
if (ctx->rich)
{
uint16_t rich_diff = compare_attrs(ctx, attrs, row, col);
if (rich_diff)
add_rich_diff(ctx, attrs, row, col, rich_diff);
}
if (cell->wc >= CELL_COMB_CHARS_LO && cell->wc <= CELL_COMB_CHARS_HI)
{
const struct composed *composed = composed_lookup(

View file

@ -9,7 +9,7 @@
struct extraction_context;
struct extraction_context *extract_begin(
enum selection_kind kind, bool strip_trailing_empty);
enum selection_kind kind, bool strip_trailing_empty, bool rich);
bool extract_one(
const struct terminal *term, const struct row *row, const struct cell *cell,
@ -19,3 +19,4 @@ bool extract_finish(
struct extraction_context *context, char **text, size_t *len);
bool extract_finish_wide(
struct extraction_context *context, char32_t **text, size_t *len);

View file

@ -1,4 +1,5 @@
#include "input.h"
#include "key-binding.h"
#include <string.h>
#include <unistd.h>
@ -340,6 +341,10 @@ execute_binding(struct seat *seat, struct terminal *term,
return true;
}
case BIND_ACTION_TOGGLE_ANSI_SELECTION:
term->ansi_selection = !(term->ansi_selection);
break;
case BIND_ACTION_SHOW_URLS_COPY:
case BIND_ACTION_SHOW_URLS_LAUNCH:
case BIND_ACTION_SHOW_URLS_PERSISTENT: {

View file

@ -33,6 +33,7 @@ enum bind_action_normal {
BIND_ACTION_PIPE_VIEW,
BIND_ACTION_PIPE_SELECTED,
BIND_ACTION_PIPE_COMMAND_OUTPUT,
BIND_ACTION_TOGGLE_ANSI_SELECTION,
BIND_ACTION_SHOW_URLS_COPY,
BIND_ACTION_SHOW_URLS_LAUNCH,
BIND_ACTION_SHOW_URLS_PERSISTENT,

View file

@ -874,7 +874,7 @@ search_extend_left(struct terminal *term, const struct coord *target)
const bool move_cursor = term->search.cursor != 0;
struct extraction_context *ctx = extract_begin(SELECTION_NONE, false);
struct extraction_context *ctx = extract_begin(SELECTION_NONE, false, false);
if (ctx == NULL)
return;
@ -938,7 +938,7 @@ search_extend_right(struct terminal *term, const struct coord *target)
const bool move_cursor = term->search.cursor == term->search.len;
struct extraction_context *ctx = extract_begin(SELECTION_NONE, false);
struct extraction_context *ctx = extract_begin(SELECTION_NONE, false, false);
if (ctx == NULL)
return;

View file

@ -328,7 +328,7 @@ selection_to_text(const struct terminal *term)
if (term->selection.coords.end.row == -1)
return NULL;
struct extraction_context *ctx = extract_begin(term->selection.kind, true);
struct extraction_context *ctx = extract_begin(term->selection.kind, true, term->ansi_selection);
if (ctx == NULL)
return NULL;

View file

@ -1400,6 +1400,9 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper,
.cb = shutdown_cb,
.cb_data = shutdown_data,
},
.ansi_pipe = conf->ansi_pipe,
.ansi_selection = false,
.foot_exe = xstrdup(foot_exe),
.cwd = xstrdup(cwd),
.grapheme_shaping = conf->tweak.grapheme_shaping,
@ -4418,9 +4421,9 @@ term_surface_kind(const struct terminal *term, const struct wl_surface *surface)
static bool
rows_to_text(const struct terminal *term, int start, int end,
int col_start, int col_end, char **text, size_t *len)
int col_start, int col_end, char **text, size_t *len, bool rich)
{
struct extraction_context *ctx = extract_begin(SELECTION_NONE, true);
struct extraction_context *ctx = extract_begin(SELECTION_NONE, true, term->ansi_pipe);
if (ctx == NULL)
return false;
@ -4476,7 +4479,7 @@ term_scrollback_to_text(const struct terminal *term, char **text, size_t *len)
end += term->grid->num_rows;
}
return rows_to_text(term, start, end, 0, term->cols, text, len);
return rows_to_text(term, start, end, 0, term->cols, text, len, true);
}
bool
@ -4484,7 +4487,7 @@ term_view_to_text(const struct terminal *term, char **text, size_t *len)
{
int start = grid_row_absolute_in_view(term->grid, 0);
int end = grid_row_absolute_in_view(term->grid, term->rows - 1);
return rows_to_text(term, start, end, 0, term->cols, text, len);
return rows_to_text(term, start, end, 0, term->cols, text, len, true);
}
bool
@ -4524,7 +4527,7 @@ term_command_output_to_text(const struct terminal *term, char **text, size_t *le
if (start_row < 0)
return false;
bool ret = rows_to_text(term, start_row, end_row, start_col, end_col, text, len);
bool ret = rows_to_text(term, start_row, end_row, start_col, end_col, text, len, true);
if (!ret)
return false;

View file

@ -807,6 +807,9 @@ struct terminal {
bool ime_reenable_after_url_mode;
const struct config_spawn_template *url_launch;
bool ansi_selection;
bool ansi_pipe;
#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED
bool ime_enabled;
#endif