diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2233fd9e..55ac7e5c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/README.md b/README.md
index b1cfb37d..c1be7476 100644
--- a/README.md
+++ b/README.md
@@ -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 command’s output
+
+The key binding `pipe-command-output` can pipe the last command’s
+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 ctrl+shift+g, the last
+command’s 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
diff --git a/config.c b/config.c
index e3d531c2..3728826f 100644
--- a/config.c
+++ b/config.c
@@ -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",
diff --git a/doc/foot.1.scd b/doc/foot.1.scd
index 385f9721..8e2fb313 100644
--- a/doc/foot.1.scd
+++ b/doc/foot.1.scd
@@ -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 command’s output
+
+The key binding *pipe-command-output* can pipe the last command’s
+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 command’s 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
diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd
index 1c87db53..14999a8b 100644
--- a/doc/foot.ini.5.scd
+++ b/doc/foot.ini.5.scd
@@ -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*
diff --git a/foot.ini b/foot.ini
index 55eb42de..42a71e58 100644
--- a/foot.ini
+++ b/foot.ini
@@ -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
diff --git a/grid.c b/grid.c
index ea103c65..e7cdedc5 100644
--- a/grid.c
+++ b/grid.c
@@ -1,5 +1,6 @@
#include "grid.h"
+#include
#include
#include
@@ -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;
}
diff --git a/input.c b/input.c
index dc0eec93..166ad70b 100644
--- a/input.c
+++ b/input.c
@@ -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) {
/* We’ve reached the bottom of the scrollback */
break;
diff --git a/key-binding.h b/key-binding.h
index 050c80a6..ba841efa 100644
--- a/key-binding.h
+++ b/key-binding.h
@@ -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,
diff --git a/osc.c b/osc.c
index ba08964c..5da8666f 100644
--- a/osc.c
+++ b/osc.c
@@ -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;
diff --git a/terminal.c b/terminal.c
index f63aaaa7..5d6c956d 100644
--- a/terminal.c
+++ b/terminal.c
@@ -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() doesn’t 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 - don’t 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
diff --git a/terminal.h b/terminal.h
index 0dca0f48..35127e3c 100644
--- a/terminal.h
+++ b/terminal.h
@@ -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);