Merge branch 'pipe-command-output'

This commit is contained in:
Daniel Eklöf 2024-02-06 12:13:27 +01:00
commit 0a302265ec
No known key found for this signature in database
GPG key ID: 5BBD4992C116573F
12 changed files with 246 additions and 51 deletions

View file

@ -58,6 +58,7 @@
of floating windows to be constrained to multiples of the cell size. of floating windows to be constrained to multiples of the cell size.
* Support for custom (i.e. other than ctrl/shift/alt/super) modifiers * Support for custom (i.e. other than ctrl/shift/alt/super) modifiers
in key bindings ([#1348][1348]). in key bindings ([#1348][1348]).
* `pipe-command-output` key binding.
[1348]: https://codeberg.org/dnkl/foot/issues/1348 [1348]: https://codeberg.org/dnkl/foot/issues/1348

View file

@ -359,6 +359,42 @@ See the
[wiki](https://codeberg.org/dnkl/foot/wiki#user-content-jumping-between-prompts) [wiki](https://codeberg.org/dnkl/foot/wiki#user-content-jumping-between-prompts)
for details, and examples for other shells. for details, and examples for other shells.
### Piping last commands output
The key binding `pipe-command-output` can pipe the last commands
output to an application of your choice (similar to the other `pipe-*`
key bindings):
```ini
[key-bindings]
pipe-command-output=[sh -c "f=$(mktemp); cat - > $f; footclient emacsclient -nw $f; rm $f"] Control+Shift+g
```
When pressing <kbd>ctrl</kbd>+<kbd>shift</kbd>+<kbd>g</kbd>, the last
commands output is written to a temporary file, then an emacsclient
is started in a new footclient instance. The temporary file is removed
after the footclient instance has closed.
For this to work, the shell must emit an OSC-133;C (`\E]133;C\E\\`)
sequence before command output starts, and an OSC-133;D
(`\E]133;D\E\\`) when the command output ends.
In fish, one way to do this is to add `preexec` and `postexec` hooks:
```fish
function foot_cmd_start --on-event fish_preexec
echo -en "\e]133;C\e\\"
end
function foot_cmd_end --on-event fish_postexec
echo -en "\e]133;D\e\\"
end
```
See the
[wiki](https://codeberg.org/dnkl/foot/wiki#user-content-piping-last-commands-output)
for details, and examples for other shells
## Alt/meta ## Alt/meta

View file

@ -111,6 +111,7 @@ static const char *const binding_action_map[] = {
[BIND_ACTION_PIPE_SCROLLBACK] = "pipe-scrollback", [BIND_ACTION_PIPE_SCROLLBACK] = "pipe-scrollback",
[BIND_ACTION_PIPE_VIEW] = "pipe-visible", [BIND_ACTION_PIPE_VIEW] = "pipe-visible",
[BIND_ACTION_PIPE_SELECTED] = "pipe-selected", [BIND_ACTION_PIPE_SELECTED] = "pipe-selected",
[BIND_ACTION_PIPE_COMMAND_OUTPUT] = "pipe-command-output",
[BIND_ACTION_SHOW_URLS_COPY] = "show-urls-copy", [BIND_ACTION_SHOW_URLS_COPY] = "show-urls-copy",
[BIND_ACTION_SHOW_URLS_LAUNCH] = "show-urls-launch", [BIND_ACTION_SHOW_URLS_LAUNCH] = "show-urls-launch",
[BIND_ACTION_SHOW_URLS_PERSISTENT] = "show-urls-persistent", [BIND_ACTION_SHOW_URLS_PERSISTENT] = "show-urls-persistent",

View file

@ -424,6 +424,38 @@ See the wiki
(https://codeberg.org/dnkl/foot/wiki#user-content-jumping-between-prompts) (https://codeberg.org/dnkl/foot/wiki#user-content-jumping-between-prompts)
for details, and examples for other shells. for details, and examples for other shells.
## Piping last commands output
The key binding *pipe-command-output* can pipe the last commands
output to an application of your choice (similar to the other
*pipe-\** key bindings):
*\[key-bindings\]++
pipe-command-output=[sh -c "f=$(mktemp); cat - > $f; footclient emacsclient -nw $f; rm $f"] Control+Shift+g*
When pressing *ctrl*+*shift*+*g*, the last commands output is written
to a temporary file, then an emacsclient is started in a new
footclient instance. The temporary file is removed after the
footclient instance has closed.
For this to work, the shell must emit an OSC-133;C (*\\E]133;C\\E\\\\*)
sequence before command output starts, and an OSC-133;D
(*\\E]133;D\\E\\\\*) when the command output ends.
In fish, one way to do this is to add _preexec_ and _postexec_ hooks:
*function foot_cmd_start --on-event fish_preexec
echo -en "\\e]133;C\\e\\\\"
end*
*function foot_cmd_end --on-event fish_postexec
echo -en "\\e]133;D\\e\\\\"
end*
See the wiki
(https://codeberg.org/dnkl/foot/wiki#user-content-piping-last-commands-output)
for details, and examples for other shells
# TERMINFO # TERMINFO
Client applications use the terminfo identifier specified by the Client applications use the terminfo identifier specified by the

View file

@ -844,11 +844,12 @@ e.g. *search-start=none*.
*fullscreen* *fullscreen*
Toggles the fullscreen state. Default: _none_. Toggles the fullscreen state. Default: _none_.
*pipe-visible*, *pipe-scrollback*, *pipe-selected* *pipe-visible*, *pipe-scrollback*, *pipe-selected*, *pipe-command-output*
Pipes the currently visible text, the entire scrollback, or the Pipes the currently visible text, the entire scrollback, the
currently selected text to an external tool. The syntax for this currently selected text, or the last command's output to an
option is a bit special; the first part of the value is the external tool. The syntax for this option is a bit special; the
command to execute enclosed in "[]", followed by the binding(s). 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 You can configure multiple pipes as long as the command strings
are different and the key bindings are unique. are different and the key bindings are unique.
@ -856,10 +857,17 @@ e.g. *search-start=none*.
Note that the command is *not* automatically run inside a shell; Note that the command is *not* automatically run inside a shell;
use *sh -c "command line"* if you need that. use *sh -c "command line"* if you need that.
Example: Example #1:
*pipe-visible=[sh -c "xurls | uniq | tac | fuzzel | xargs -r
# Extract currently visible URLs, let user choose one (via
fuzzel), then launch firefox with the selected URL++
*pipe-visible=[sh -c "xurls | uniq | tac | fuzzel | xargs -r
firefox"] Control+Print* firefox"] Control+Print*
Example #2:
# Write scrollback content to /tmp/foot-scrollback.txt++
*pipe-scrollback=[sh -c "cat - > /tmp/foot-scrollback.txt"]
Control+Shift+Print*
Default: _none_ Default: _none_
*show-urls-launch* *show-urls-launch*

View file

@ -158,6 +158,7 @@
# pipe-visible=[sh -c "xurls | fuzzel | xargs -r firefox"] none # pipe-visible=[sh -c "xurls | fuzzel | xargs -r firefox"] none
# pipe-scrollback=[sh -c "xurls | fuzzel | xargs -r firefox"] none # pipe-scrollback=[sh -c "xurls | fuzzel | xargs -r firefox"] none
# pipe-selected=[xargs -r firefox] none # pipe-selected=[xargs -r firefox] none
# pipe-command-output=[sh -c "cat - > /tmp/foot-cmd-out.txt"] none # Write output of last command to /tmp/foot-cmd-out.txt (requires shell integration)
# show-urls-launch=Control+Shift+o # show-urls-launch=Control+Shift+o
# show-urls-copy=none # show-urls-copy=none
# show-urls-persistent=none # show-urls-persistent=none

70
grid.c
View file

@ -1,5 +1,6 @@
#include "grid.h" #include "grid.h"
#include <limits.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
@ -231,7 +232,7 @@ grid_snapshot(const struct grid *grid)
clone_row->cells = xmalloc(grid->num_cols * sizeof(clone_row->cells[0])); clone_row->cells = xmalloc(grid->num_cols * sizeof(clone_row->cells[0]));
clone_row->linebreak = row->linebreak; clone_row->linebreak = row->linebreak;
clone_row->dirty = row->dirty; clone_row->dirty = row->dirty;
clone_row->prompt_marker = row->prompt_marker; clone_row->shell_integration = row->shell_integration;
for (int c = 0; c < grid->num_cols; c++) for (int c = 0; c < grid->num_cols; c++)
clone_row->cells[c] = row->cells[c]; clone_row->cells[c] = row->cells[c];
@ -366,7 +367,9 @@ grid_row_alloc(int cols, bool initialize)
row->dirty = false; row->dirty = false;
row->linebreak = false; row->linebreak = false;
row->extra = NULL; row->extra = NULL;
row->prompt_marker = false; row->shell_integration.prompt_marker = false;
row->shell_integration.cmd_start = -1;
row->shell_integration.cmd_end = -1;
if (initialize) { if (initialize) {
row->cells = xcalloc(cols, sizeof(row->cells[0])); row->cells = xcalloc(cols, sizeof(row->cells[0]));
@ -425,7 +428,9 @@ grid_resize_without_reflow(
new_row->dirty = old_row->dirty; new_row->dirty = old_row->dirty;
new_row->linebreak = false; new_row->linebreak = false;
new_row->prompt_marker = old_row->prompt_marker; new_row->shell_integration.prompt_marker = old_row->shell_integration.prompt_marker;
new_row->shell_integration.cmd_start = min(old_row->shell_integration.cmd_start, new_cols - 1);
new_row->shell_integration.cmd_end = min(old_row->shell_integration.cmd_end, new_cols - 1);
if (new_cols > old_cols) { if (new_cols > old_cols) {
/* Clear "new" columns */ /* Clear "new" columns */
@ -587,7 +592,9 @@ _line_wrap(struct grid *old_grid, struct row **new_grid, struct row *row,
/* Scrollback is full, need to reuse a row */ /* Scrollback is full, need to reuse a row */
grid_row_reset_extra(new_row); grid_row_reset_extra(new_row);
new_row->linebreak = false; new_row->linebreak = false;
new_row->prompt_marker = false; new_row->shell_integration.prompt_marker = false;
new_row->shell_integration.cmd_start = -1;
new_row->shell_integration.cmd_end = -1;
tll_foreach(old_grid->sixel_images, it) { tll_foreach(old_grid->sixel_images, it) {
if (it->item.pos.row == *row_idx) { if (it->item.pos.row == *row_idx) {
@ -831,35 +838,26 @@ grid_resize_and_reflow(
int end; int end;
bool tp_break = false; bool tp_break = false;
bool uri_break = false; bool uri_break = false;
bool ftcs_break = false;
/* /* Figure out where to end this chunk */
* Set end-coordinate for this chunk, by finding the next {
* point-of-interest on this row. const int uri_col = range != range_terminator
* ? ((range->start >= start ? range->start : range->end) + 1)
* If there are no more tracking points, or URI ranges, : INT_MAX;
* the end-coordinate will be at the end of the row, const int tp_col = tp != NULL ? tp->col + 1 : INT_MAX;
*/ const int ftcs_col = old_row->shell_integration.cmd_start >= start
if (range != range_terminator) { ? old_row->shell_integration.cmd_start + 1
int uri_col = (range->start >= start ? range->start : range->end) + 1; : old_row->shell_integration.cmd_end >= start
? old_row->shell_integration.cmd_end + 1
: INT_MAX;
if (tp != NULL) { end = min(col_count, min(min(tp_col, uri_col), ftcs_col));
int tp_col = tp->col + 1;
end = min(tp_col, uri_col);
tp_break = end == tp_col; uri_break = end == uri_col;
uri_break = end == uri_col; tp_break = end == tp_col;
LOG_DBG("tp+uri break at %d (%d, %d)", end, tp_col, uri_col); ftcs_break = end == ftcs_col;
} else { }
end = uri_col;
uri_break = true;
LOG_DBG("uri break at %d", end);
}
} else if (tp != NULL) {
end = tp->col + 1;
tp_break = true;
LOG_DBG("TP break at %d", end);
} else
end = col_count;
int cols = end - start; int cols = end - start;
xassert(cols > 0); xassert(cols > 0);
@ -920,7 +918,7 @@ grid_resize_and_reflow(
xassert(from + amount <= old_cols); xassert(from + amount <= old_cols);
if (from == 0) if (from == 0)
new_row->prompt_marker = old_row->prompt_marker; new_row->shell_integration.prompt_marker = old_row->shell_integration.prompt_marker;
memcpy( memcpy(
&new_row->cells[new_col_idx], &old_row->cells[from], &new_row->cells[new_col_idx], &old_row->cells[from],
@ -979,6 +977,16 @@ grid_resize_and_reflow(
} }
} }
if (ftcs_break) {
xassert(old_row->shell_integration.cmd_start == start + cols - 1 ||
old_row->shell_integration.cmd_end == start + cols - 1);
if (old_row->shell_integration.cmd_start == start + cols - 1)
new_row->shell_integration.cmd_start = new_col_idx - 1;
if (old_row->shell_integration.cmd_end == start + cols - 1)
new_row->shell_integration.cmd_end = new_col_idx - 1;
}
left -= cols; left -= cols;
start += cols; start += cols;
} }

11
input.c
View file

@ -227,7 +227,8 @@ execute_binding(struct seat *seat, struct terminal *term,
break; break;
/* FALLTHROUGH */ /* FALLTHROUGH */
case BIND_ACTION_PIPE_VIEW: case BIND_ACTION_PIPE_VIEW:
case BIND_ACTION_PIPE_SELECTED: { case BIND_ACTION_PIPE_SELECTED:
case BIND_ACTION_PIPE_COMMAND_OUTPUT: {
if (binding->aux->type != BINDING_AUX_PIPE) if (binding->aux->type != BINDING_AUX_PIPE)
return true; return true;
@ -269,6 +270,10 @@ execute_binding(struct seat *seat, struct terminal *term,
len = text != NULL ? strlen(text) : 0; len = text != NULL ? strlen(text) : 0;
break; break;
case BIND_ACTION_PIPE_COMMAND_OUTPUT:
success = term_command_output_to_text(term, &text, &len);
break;
default: default:
BUG("Unhandled action type"); BUG("Unhandled action type");
success = false; success = false;
@ -377,7 +382,7 @@ execute_binding(struct seat *seat, struct terminal *term,
const struct row *row = grid->rows[r_abs]; const struct row *row = grid->rows[r_abs];
xassert(row != NULL); xassert(row != NULL);
if (!row->prompt_marker) if (!row->shell_integration.prompt_marker)
continue; continue;
grid->view = r_abs; grid->view = r_abs;
@ -409,7 +414,7 @@ execute_binding(struct seat *seat, struct terminal *term,
const struct row *row = grid->rows[r_abs]; const struct row *row = grid->rows[r_abs];
xassert(row != NULL); xassert(row != NULL);
if (!row->prompt_marker) { if (!row->shell_integration.prompt_marker) {
if (r_abs == grid->offset + term->rows - 1) { if (r_abs == grid->offset + term->rows - 1) {
/* Weve reached the bottom of the scrollback */ /* Weve reached the bottom of the scrollback */
break; break;

View file

@ -32,6 +32,7 @@ enum bind_action_normal {
BIND_ACTION_PIPE_SCROLLBACK, BIND_ACTION_PIPE_SCROLLBACK,
BIND_ACTION_PIPE_VIEW, BIND_ACTION_PIPE_VIEW,
BIND_ACTION_PIPE_SELECTED, BIND_ACTION_PIPE_SELECTED,
BIND_ACTION_PIPE_COMMAND_OUTPUT,
BIND_ACTION_SHOW_URLS_COPY, BIND_ACTION_SHOW_URLS_COPY,
BIND_ACTION_SHOW_URLS_LAUNCH, BIND_ACTION_SHOW_URLS_LAUNCH,
BIND_ACTION_SHOW_URLS_PERSISTENT, BIND_ACTION_SHOW_URLS_PERSISTENT,

12
osc.c
View file

@ -893,7 +893,7 @@ osc_dispatch(struct terminal *term)
term->grid->cursor.point.row, term->grid->cursor.point.row,
term->grid->cursor.point.col); term->grid->cursor.point.col);
term->grid->cur_row->prompt_marker = true; term->grid->cur_row->shell_integration.prompt_marker = true;
break; break;
case 'B': case 'B':
@ -901,11 +901,17 @@ osc_dispatch(struct terminal *term)
break; break;
case 'C': case 'C':
LOG_DBG("FTCS_COMMAND_EXECUTED"); LOG_DBG("FTCS_COMMAND_EXECUTED: %dx%d",
term->grid->cursor.point.row,
term->grid->cursor.point.col);
term->grid->cur_row->shell_integration.cmd_start = term->grid->cursor.point.col;
break; break;
case 'D': case 'D':
LOG_DBG("FTCS_COMMAND_FINISHED"); LOG_DBG("FTCS_COMMAND_FINISHED: %dx%d",
term->grid->cursor.point.row,
term->grid->cursor.point.col);
term->grid->cur_row->shell_integration.cmd_end = term->grid->cursor.point.col;
break; break;
} }
break; break;

View file

@ -1825,7 +1825,9 @@ erase_line(struct terminal *term, struct row *row)
{ {
erase_cell_range(term, row, 0, term->cols - 1); erase_cell_range(term, row, 0, term->cols - 1);
row->linebreak = false; row->linebreak = false;
row->prompt_marker = false; row->shell_integration.prompt_marker = false;
row->shell_integration.cmd_start = -1;
row->shell_integration.cmd_end = -1;
} }
void void
@ -3611,7 +3613,7 @@ term_surface_kind(const struct terminal *term, const struct wl_surface *surface)
static bool static bool
rows_to_text(const struct terminal *term, int start, int end, rows_to_text(const struct terminal *term, int start, int end,
char **text, size_t *len) int col_start, int col_end, char **text, size_t *len)
{ {
struct extraction_context *ctx = extract_begin(SELECTION_NONE, true); struct extraction_context *ctx = extract_begin(SELECTION_NONE, true);
if (ctx == NULL) if (ctx == NULL)
@ -3624,15 +3626,20 @@ rows_to_text(const struct terminal *term, int start, int end,
const struct row *row = term->grid->rows[r]; const struct row *row = term->grid->rows[r];
xassert(row != NULL); xassert(row != NULL);
for (int c = 0; c < term->cols; c++) const int c_end = r == end ? col_end : term->cols;
for (int c = col_start; c < c_end; c++) {
if (!extract_one(term, row, &row->cells[c], c, ctx)) if (!extract_one(term, row, &row->cells[c], c, ctx))
goto out; goto out;
}
if (r == end) if (r == end)
break; break;
r++; r++;
r &= grid_rows - 1; r &= grid_rows - 1;
col_start = 0;
} }
out: out:
@ -3664,7 +3671,7 @@ term_scrollback_to_text(const struct terminal *term, char **text, size_t *len)
end += term->grid->num_rows; end += term->grid->num_rows;
} }
return rows_to_text(term, start, end, text, len); return rows_to_text(term, start, end, 0, term->cols, text, len);
} }
bool bool
@ -3672,7 +3679,91 @@ term_view_to_text(const struct terminal *term, char **text, size_t *len)
{ {
int start = grid_row_absolute_in_view(term->grid, 0); int start = grid_row_absolute_in_view(term->grid, 0);
int end = grid_row_absolute_in_view(term->grid, term->rows - 1); int end = grid_row_absolute_in_view(term->grid, term->rows - 1);
return rows_to_text(term, start, end, text, len); return rows_to_text(term, start, end, 0, term->cols, text, len);
}
bool
term_command_output_to_text(const struct terminal *term, char **text, size_t *len)
{
int start_row = -1;
int end_row = -1;
int start_col = -1;
int end_col = -1;
const struct grid *grid = term->grid;
const int sb_end = grid_row_absolute(grid, term->rows - 1);
const int sb_start = (sb_end + 1) & (grid->num_rows - 1);
int r = sb_end;
while (start_row < 0) {
const struct row *row = grid->rows[r];
if (row == NULL)
break;
if (row->shell_integration.cmd_end >= 0) {
end_row = r;
end_col = row->shell_integration.cmd_end;
}
if (end_row >= 0 && row->shell_integration.cmd_start >= 0) {
start_row = r;
start_col = row->shell_integration.cmd_start;
}
if (r == sb_start)
break;
r = (r - 1 + grid->num_rows) & (grid->num_rows - 1);
}
if (start_row < 0)
return false;
bool ret = rows_to_text(term, start_row, end_row, start_col, end_col, text, len);
if (!ret)
return false;
/*
* If the FTCS_COMMAND_FINISHED marker was emitted at the *first*
* column, then the *entire* previous line is part of the command
* output. *Including* the newline, if any.
*
* Since rows_to_text() doesnt extract the column
* FTCS_COMMAND_FINISHED was emitted at (that would be wrong -
* FTCS_COMMAND_FINISHED is emitted *after* the command output,
* not at its last character), the extraction logic will not see
* the last newline (this is true for all non-line-wise selection
* types), and the extracted text will *not* end with a newline.
*
* Here we try to compensate for that. Note that if end_col is
* not 0, then the command output only covers a partial row, and
* thus we do *not* want to append a newline.
*/
if (end_col > 0) {
/* Command output covers partial row - dont append newline */
return true;
}
int next_to_last_row = (end_row - 1 + grid->num_rows) & (grid->num_rows - 1);
const struct row *row = grid->rows[next_to_last_row];
/* Add newline if last row has a hard linebreak */
if (row->linebreak) {
char *new_text = xrealloc(*text, *len + 1 + 1);
if (new_text == NULL) {
/* Ignore failure - use text as is (without inserting newline) */
return true;
}
*text = new_text;
(*len)++;
(*text)[*len - 1] = '\n';
(*text)[*len] = '\0';
}
return true;
} }
bool bool

View file

@ -121,8 +121,11 @@ struct row {
bool dirty; bool dirty;
bool linebreak; bool linebreak;
/* Shell integration */ struct {
bool prompt_marker; bool prompt_marker;
int cmd_start; /* Column, -1 if unset */
int cmd_end; /* Column, -1 if unset */
} shell_integration;
}; };
struct sixel { struct sixel {
@ -844,6 +847,8 @@ bool term_scrollback_to_text(
const struct terminal *term, char **text, size_t *len); const struct terminal *term, char **text, size_t *len);
bool term_view_to_text( bool term_view_to_text(
const struct terminal *term, char **text, size_t *len); const struct terminal *term, char **text, size_t *len);
bool term_command_output_to_text(
const struct terminal *term, char **text, size_t *len);
bool term_ime_is_enabled(const struct terminal *term); bool term_ime_is_enabled(const struct terminal *term);
void term_ime_enable(struct terminal *term); void term_ime_enable(struct terminal *term);