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.
* Support for custom (i.e. other than ctrl/shift/alt/super) modifiers
in key bindings ([#1348][1348]).
* `pipe-command-output` key binding.
[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)
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

View file

@ -111,6 +111,7 @@ static const char *const binding_action_map[] = {
[BIND_ACTION_PIPE_SCROLLBACK] = "pipe-scrollback",
[BIND_ACTION_PIPE_VIEW] = "pipe-visible",
[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_LAUNCH] = "show-urls-launch",
[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)
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
Client applications use the terminfo identifier specified by the

View file

@ -844,11 +844,12 @@ e.g. *search-start=none*.
*fullscreen*
Toggles the fullscreen state. Default: _none_.
*pipe-visible*, *pipe-scrollback*, *pipe-selected*
Pipes the currently visible text, the entire scrollback, or the
currently selected text 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).
*pipe-visible*, *pipe-scrollback*, *pipe-selected*, *pipe-command-output*
Pipes the currently visible text, the entire scrollback, the
currently selected text, or the last command's output 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.
@ -856,10 +857,17 @@ e.g. *search-start=none*.
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 | uniq | tac | fuzzel | xargs -r
Example #1:
# 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*
Example #2:
# Write scrollback content to /tmp/foot-scrollback.txt++
*pipe-scrollback=[sh -c "cat - > /tmp/foot-scrollback.txt"]
Control+Shift+Print*
Default: _none_
*show-urls-launch*

View file

@ -158,6 +158,7 @@
# pipe-visible=[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-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-copy=none
# show-urls-persistent=none

70
grid.c
View file

@ -1,5 +1,6 @@
#include "grid.h"
#include <limits.h>
#include <stdlib.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->linebreak = row->linebreak;
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++)
clone_row->cells[c] = row->cells[c];
@ -366,7 +367,9 @@ grid_row_alloc(int cols, bool initialize)
row->dirty = false;
row->linebreak = false;
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) {
row->cells = xcalloc(cols, sizeof(row->cells[0]));
@ -425,7 +428,9 @@ grid_resize_without_reflow(
new_row->dirty = old_row->dirty;
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) {
/* 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 */
grid_row_reset_extra(new_row);
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) {
if (it->item.pos.row == *row_idx) {
@ -831,35 +838,26 @@ grid_resize_and_reflow(
int end;
bool tp_break = false;
bool uri_break = false;
bool ftcs_break = false;
/*
* Set end-coordinate for this chunk, by finding the next
* point-of-interest on this row.
*
* If there are no more tracking points, or URI ranges,
* the end-coordinate will be at the end of the row,
*/
if (range != range_terminator) {
int uri_col = (range->start >= start ? range->start : range->end) + 1;
/* Figure out where to end this chunk */
{
const int uri_col = range != range_terminator
? ((range->start >= start ? range->start : range->end) + 1)
: INT_MAX;
const int tp_col = tp != NULL ? tp->col + 1 : INT_MAX;
const int ftcs_col = old_row->shell_integration.cmd_start >= start
? old_row->shell_integration.cmd_start + 1
: old_row->shell_integration.cmd_end >= start
? old_row->shell_integration.cmd_end + 1
: INT_MAX;
if (tp != NULL) {
int tp_col = tp->col + 1;
end = min(tp_col, uri_col);
end = min(col_count, min(min(tp_col, uri_col), ftcs_col));
tp_break = end == tp_col;
uri_break = end == uri_col;
LOG_DBG("tp+uri break at %d (%d, %d)", end, tp_col, uri_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;
uri_break = end == uri_col;
tp_break = end == tp_col;
ftcs_break = end == ftcs_col;
}
int cols = end - start;
xassert(cols > 0);
@ -920,7 +918,7 @@ grid_resize_and_reflow(
xassert(from + amount <= old_cols);
if (from == 0)
new_row->prompt_marker = old_row->prompt_marker;
new_row->shell_integration.prompt_marker = old_row->shell_integration.prompt_marker;
memcpy(
&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;
start += cols;
}

11
input.c
View file

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

View file

@ -32,6 +32,7 @@ enum bind_action_normal {
BIND_ACTION_PIPE_SCROLLBACK,
BIND_ACTION_PIPE_VIEW,
BIND_ACTION_PIPE_SELECTED,
BIND_ACTION_PIPE_COMMAND_OUTPUT,
BIND_ACTION_SHOW_URLS_COPY,
BIND_ACTION_SHOW_URLS_LAUNCH,
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.col);
term->grid->cur_row->prompt_marker = true;
term->grid->cur_row->shell_integration.prompt_marker = true;
break;
case 'B':
@ -901,11 +901,17 @@ osc_dispatch(struct terminal *term)
break;
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;
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;

View file

@ -1825,7 +1825,9 @@ erase_line(struct terminal *term, struct row *row)
{
erase_cell_range(term, row, 0, term->cols - 1);
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
@ -3611,7 +3613,7 @@ 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,
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);
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];
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))
goto out;
}
if (r == end)
break;
r++;
r &= grid_rows - 1;
col_start = 0;
}
out:
@ -3664,7 +3671,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, text, len);
return rows_to_text(term, start, end, 0, term->cols, text, len);
}
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 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

View file

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