diff --git a/CHANGELOG.md b/CHANGELOG.md index 7737f108..7a84bc13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,10 +47,13 @@ * `-Dsystemd-units-dir=` meson command line option. * Support for custom environment variables in `foot.ini` ([#1070][1070]). - +* Support for jumping to previous/next prompt (requires shell + integration). By default bound to `ctrl`+`shift`+`z` and + `ctrl`+`shift`+`x` respectively ([#30][30]). [1058]: https://codeberg.org/dnkl/foot/issues/1058 [1070]: https://codeberg.org/dnkl/foot/issues/1070 +[30]: https://codeberg.org/dnkl/foot/issues/30 ### Changed diff --git a/README.md b/README.md index 227c528c..348f2e18 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,9 @@ The fast, lightweight and minimalistic Wayland terminal emulator. 1. [Mouse](#mouse) 1. [Server (daemon) mode](#server-daemon-mode) 1. [URLs](#urls) +1. [Shell integration](#shell-integration) + 1. [Current working directory](#current-working-directory) + 1. [Jumping between prompts](#jumping-between-prompts) 1. [Alt/meta](#alt-meta) 1. [Backspace](#backspace) 1. [Keypad](#keypad) @@ -157,13 +160,21 @@ These are the default shortcuts. See `man foot.ini` and the example ctrl+shift+n : Spawn a new terminal. If the shell has been [configured to emit the OSC 7 escape - sequence](https://codeberg.org/dnkl/foot/wiki#user-content-how-to-configure-my-shell-to-emit-the-osc-7-escape-sequence), + sequence](https://codeberg.org/dnkl/foot/wiki#user-content-spawning-new-terminal-instances-in-the-current-working-directory), the new terminal will start in the current working directory. ctrl+shift+u : Enter URL mode, where all currently visible URLs are tagged with a jump label with a key sequence that will open the URL. +ctrl+shift+z +: Jump to the previous, currently not visible, prompt. Requires [shell + integration](https://codeberg.org/dnkl/foot/wiki#user-content-jumping-between-prompts). + +ctrl+shift+x +: Jump to the next prompt. Requires [shell + integration](https://codeberg.org/dnkl/foot/wiki#user-content-jumping-between-prompts). + #### Scrollback search @@ -296,6 +307,44 @@ Jump label colors, the URL underline color, and the letters used in the jump label key sequences can be configured. +## Shell integration + +### Current working directory + +New foot terminal instances (bound to +ctrl+shift+n by default) will open in +the current working directory, **if** the shell in the “parent” +terminal reports directory changes. + +This is done with the OSC-7 escape sequence. Most shells can be +scripted to do this, if they do not support it natively. See the +[wiki](https://codeberg.org/dnkl/foot/wiki#user-content-spawning-new-terminal-instances-in-the-current-working-directory) +for details. + + +### Jumping between prompts + +Foot can move the current viewport to focus prompts of already +executed commands (bound to +ctrl+shift+z/x by +default). + +For this to work, the shell needs to emit an OSC-133;A +(`\E]133;A\E\\`) sequence before each prompt. + +In zsh, one way to do this is to add a `precmd` hook: + +```zsh +precmd() { + print -Pn "\e]133;A\e\\" +} +``` + +See the +[wiki](https://codeberg.org/dnkl/foot/wiki#user-content-jumping-between-prompts) +for details, and examples for other shells. + + ## Alt/meta By default, foot prefixes _Meta characters_ with ESC. This corresponds @@ -392,7 +441,7 @@ with the terminal emulator itself. Foot implements the following OSCs: supported) * `OSC 2` - change window title * `OSC 4` - change color palette -* `OSC 7` - report CWD +* `OSC 7` - report CWD (see [shell integration](#shell-integration)) * `OSC 8` - hyperlink * `OSC 9` - desktop notification * `OSC 10` - change (default) foreground color @@ -408,6 +457,7 @@ with the terminal emulator itself. Foot implements the following OSCs: * `OSC 112` - reset cursor color * `OSC 117` - reset highlight background color * `OSC 119` - reset highlight foreground color +* `OSC 133` - [shell integration](#shell-integration) * `OSC 555` - flash screen (**foot specific**) * `OSC 777` - desktop notification (only the `;notify` sub-command of OSC 777 is supported.) diff --git a/commands.c b/commands.c index 85e200e2..7b93f044 100644 --- a/commands.c +++ b/commands.c @@ -19,23 +19,14 @@ cmd_scrollback_up(struct terminal *term, int rows) return; const struct grid *grid = term->grid; - const int offset = grid->offset; const int view = grid->view; const int grid_rows = grid->num_rows; - const int screen_rows = term->rows; - - int scrollback_start = (offset + screen_rows) & (grid_rows - 1); - - /* Part of the scrollback may be uninitialized */ - while (grid->rows[scrollback_start] == NULL) { - scrollback_start++; - scrollback_start &= grid_rows - 1; - } /* The view row number in scrollback relative coordinates. This is * the maximum number of rows we’re allowed to scroll */ - int view_sb_rel = view - scrollback_start + grid_rows; - view_sb_rel &= grid_rows - 1; + int sb_start = grid_sb_start_ignore_uninitialized(grid, term->rows); + int view_sb_rel = + grid_row_abs_to_sb_precalc_sb_start(grid, sb_start, view); rows = min(rows, view_sb_rel); if (rows == 0) diff --git a/config.c b/config.c index d2be087b..ce0e27b8 100644 --- a/config.c +++ b/config.c @@ -115,6 +115,8 @@ static const char *const binding_action_map[] = { [BIND_ACTION_SHOW_URLS_LAUNCH] = "show-urls-launch", [BIND_ACTION_SHOW_URLS_PERSISTENT] = "show-urls-persistent", [BIND_ACTION_TEXT_BINDING] = "text-binding", + [BIND_ACTION_PROMPT_PREV] = "prompt-prev", + [BIND_ACTION_PROMPT_NEXT] = "prompt-next", /* Mouse-specific actions */ [BIND_ACTION_SELECT_BEGIN] = "select-begin", @@ -2663,6 +2665,8 @@ add_default_key_bindings(struct config *conf) {BIND_ACTION_FONT_SIZE_RESET, m_ctrl, {{XKB_KEY_KP_0}}}, {BIND_ACTION_SPAWN_TERMINAL, m_ctrl_shift, {{XKB_KEY_n}}}, {BIND_ACTION_SHOW_URLS_LAUNCH, m_ctrl_shift, {{XKB_KEY_u}}}, + {BIND_ACTION_PROMPT_PREV, m_ctrl_shift, {{XKB_KEY_z}}}, + {BIND_ACTION_PROMPT_NEXT, m_ctrl_shift, {{XKB_KEY_x}}}, }; conf->bindings.key.count = ALEN(bindings); diff --git a/doc/foot-ctlseqs.7.scd b/doc/foot-ctlseqs.7.scd index 2759b625..1d94219f 100644 --- a/doc/foot-ctlseqs.7.scd +++ b/doc/foot-ctlseqs.7.scd @@ -677,6 +677,9 @@ All _OSC_ sequences begin with *\\E]*, sometimes abbreviated _OSC_. | \\E] 119 \\E\\ : xterm : Reset selection foreground color +| \\E] 133 ; A \\E\\ +: FinalTerm +: Mark start of shell prompt | \\E] 555 \\E\\ : foot : Flash the entire terminal (foot extension) diff --git a/doc/foot.1.scd b/doc/foot.1.scd index 6f0ad6db..afb8faf5 100644 --- a/doc/foot.1.scd +++ b/doc/foot.1.scd @@ -205,6 +205,13 @@ default) available; see *foot.ini*(5). *ctrl*+*shift*+*u* Activate URL mode, allowing you to "launch" URLs. +*ctrl*+*shift*+*z* + Jump to the previous, currently not visible, prompt. Requires + shell integration. + +*ctrl*+*shift*+*x* + Jump to the next prompt. Requires shell integration. + ## SCROLLBACK SEARCH *ctrl*+*r* @@ -370,6 +377,38 @@ commented out will usually be installed to */etc/xdg/foot/foot.ini*. For more information, see *foot.ini*(5). +# SHELL INTEGRATION + +## Current working directory + +New foot terminal instances (bound to *ctrl*+*shift*+*n* by default) +will open in the current working directory, if the shell in the +“parent” terminal reports directory changes. + +This is done with the OSC-7 escape sequence. Most shells can be +scripted to do this, if they do not support it natively. See the wiki +(https://codeberg.org/dnkl/foot/wiki#user-content-spawning-new-terminal-instances-in-the-current-working-directory) +for details. + + +## Jumping between prompts + +Foot can move the current viewport to focus prompts of already +executed commands (bound to *ctrl*+*shift*+*z*/*x* by default). + +For this to work, the shell needs to emit an OSC-133;A +(*\\E]133;A\\E\\\\*) sequence before each prompt. + +In zsh, one way to do this is to add a _precmd_ hook: + + *precmd() { + print -Pn "\\e]133;A\\e\\\\" + }* + +See the wiki +(https://codeberg.org/dnkl/foot/wiki#user-content-jumping-between-prompts) +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 bc1b6bdd..bd551442 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -773,6 +773,14 @@ e.g. *search-start=none*. jump label with a key sequence that will place the URL in the clipboard. Default: _none_. +*prompt-prev* + Jump to the previous, currently not visible, prompt (requires + shell integration, see *foot*(1)). Default: _Control+Shift+z_. + +*prompt-next* + Jump the next prompt (requires shell integration, see + *foot*(1)). Default: _Control+Shift+x_. + # SECTION: search-bindings This section lets you override the default key bindings used in diff --git a/foot.ini b/foot.ini index f4dc4280..10eb56e8 100644 --- a/foot.ini +++ b/foot.ini @@ -146,6 +146,8 @@ # show-urls-launch=Control+Shift+u # show-urls-copy=none # show-urls-persistent=none +# prompt-prev=Control+Shift+z +# prompt-next=Control+Shift+x # noop=none [search-bindings] diff --git a/grid.c b/grid.c index bc1018d9..a21f24a2 100644 --- a/grid.c +++ b/grid.c @@ -24,6 +24,7 @@ * scrollback, with the *highest* number being at the bottom of the * screen, where new input appears. */ + int grid_row_abs_to_sb(const struct grid *grid, int screen_rows, int abs_row) { @@ -43,6 +44,38 @@ int grid_row_sb_to_abs(const struct grid *grid, int screen_rows, int sb_rel_row) return abs_row; } +int +grid_sb_start_ignore_uninitialized(const struct grid *grid, int screen_rows) +{ + int scrollback_start = grid->offset + screen_rows; + scrollback_start &= grid->num_rows - 1; + + while (grid->rows[scrollback_start] == NULL) { + scrollback_start++; + scrollback_start &= grid->num_rows - 1; + } + + return scrollback_start; +} + +int +grid_row_abs_to_sb_precalc_sb_start(const struct grid *grid, int sb_start, + int abs_row) +{ + int rebased_row = abs_row - sb_start + grid->num_rows; + rebased_row &= grid->num_rows - 1; + return rebased_row; +} + +int +grid_row_sb_to_abs_precalc_sb_start(const struct grid *grid, int sb_start, + int sb_rel_row) +{ + int abs_row = sb_rel_row + sb_start; + abs_row &= grid->num_rows - 1; + return abs_row; +} + static void ensure_row_has_extra_data(struct row *row) { @@ -196,6 +229,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; for (int c = 0; c < grid->num_cols; c++) clone_row->cells[c] = row->cells[c]; @@ -286,6 +320,7 @@ grid_row_alloc(int cols, bool initialize) row->dirty = false; row->linebreak = true; row->extra = NULL; + row->prompt_marker = false; if (initialize) { row->cells = xcalloc(cols, sizeof(row->cells[0])); @@ -344,6 +379,7 @@ grid_resize_without_reflow( new_row->dirty = old_row->dirty; new_row->linebreak = false; + new_row->prompt_marker = old_row->prompt_marker; if (new_cols > old_cols) { /* Clear "new" columns */ @@ -503,6 +539,7 @@ _line_wrap(struct grid *old_grid, struct row **new_grid, struct row *row, /* Scrollback is full, need to re-use a row */ grid_row_reset_extra(new_row); new_row->linebreak = true; + new_row->prompt_marker = false; tll_foreach(old_grid->sixel_images, it) { if (it->item.pos.row == *row_idx) { @@ -834,6 +871,9 @@ grid_resize_and_reflow( xassert(new_col_idx + amount <= new_cols); xassert(from + amount <= old_cols); + if (from == 0) + new_row->prompt_marker = old_row->prompt_marker; + memcpy( &new_row->cells[new_col_idx], &old_row->cells[from], amount * sizeof(struct cell)); diff --git a/grid.h b/grid.h index 22bd76bb..0664409c 100644 --- a/grid.h +++ b/grid.h @@ -25,6 +25,12 @@ void grid_resize_and_reflow( int grid_row_abs_to_sb(const struct grid *grid, int screen_rows, int abs_row); int grid_row_sb_to_abs(const struct grid *grid, int screen_rows, int sb_rel_row); +int grid_sb_start_ignore_uninitialized(const struct grid *grid, int screen_rows); +int grid_row_abs_to_sb_precalc_sb_start( + const struct grid *grid, int sb_start, int abs_row); +int grid_row_sb_to_abs_precalc_sb_start( + const struct grid *grid, int sb_start, int sb_rel_row); + static inline int grid_row_absolute(const struct grid *grid, int row_no) { @@ -37,7 +43,6 @@ grid_row_absolute_in_view(const struct grid *grid, int row_no) return (grid->view + row_no) & (grid->num_rows - 1); } - static inline struct row * _grid_row_maybe_alloc(struct grid *grid, int row_no, bool alloc_if_null) { diff --git a/input.c b/input.c index 916cbd38..fca46050 100644 --- a/input.c +++ b/input.c @@ -23,8 +23,9 @@ #define LOG_MODULE "input" #define LOG_ENABLE_DBG 0 #include "log.h" -#include "config.h" #include "commands.h" +#include "config.h" +#include "grid.h" #include "keymap.h" #include "kitty-keymap.h" #include "macros.h" @@ -335,6 +336,86 @@ execute_binding(struct seat *seat, struct terminal *term, term_to_slave(term, binding->aux->text.data, binding->aux->text.len); return true; + case BIND_ACTION_PROMPT_PREV: { + if (term->grid != &term->normal) + return false; + + struct grid *grid = term->grid; + const int sb_start = + grid_sb_start_ignore_uninitialized(grid, term->rows); + + /* Check each row from current view-1 (that is, the first + * currently not visible row), up to, and including, the + * scrollback start */ + for (int r_sb_rel = + grid_row_abs_to_sb_precalc_sb_start( + grid, sb_start, grid->view) - 1; + r_sb_rel >= 0; r_sb_rel--) + { + const int r_abs = + grid_row_sb_to_abs_precalc_sb_start(grid, sb_start, r_sb_rel); + + const struct row *row = grid->rows[r_abs]; + xassert(row != NULL); + + if (!row->prompt_marker) + continue; + + grid->view = r_abs; + term_damage_view(term); + render_refresh(term); + break; + } + + return true; + } + + case BIND_ACTION_PROMPT_NEXT: { + if (term->grid != &term->normal) + return false; + + struct grid *grid = term->grid; + const int num_rows = grid->num_rows; + + if (grid->view == grid->offset) { + /* Already at the bottom */ + return true; + } + + /* Check each row from view+1, to the bottom of the scrollback */ + for (int r_abs = (grid->view + 1) & (num_rows - 1); + ; + r_abs = (r_abs + 1) & (num_rows - 1)) + { + const struct row *row = grid->rows[r_abs]; + xassert(row != NULL); + + if (!row->prompt_marker) { + if (r_abs == grid->offset + term->rows - 1) { + /* We’ve reached the bottom of the scrollback */ + break; + } + continue; + } + + int sb_start = grid_sb_start_ignore_uninitialized(grid, term->rows); + int ofs_sb_rel = + grid_row_abs_to_sb_precalc_sb_start(grid, sb_start, grid->offset); + int new_view_sb_rel = + grid_row_abs_to_sb_precalc_sb_start(grid, sb_start, r_abs); + + new_view_sb_rel = min(ofs_sb_rel, new_view_sb_rel); + grid->view = grid_row_sb_to_abs_precalc_sb_start( + grid, sb_start, new_view_sb_rel); + + term_damage_view(term); + render_refresh(term); + break; + } + + return true; + } + case BIND_ACTION_SELECT_BEGIN: selection_start( term, seat->mouse.col, seat->mouse.row, SELECTION_CHAR_WISE, false); diff --git a/key-binding.h b/key-binding.h index f30184c5..179b978b 100644 --- a/key-binding.h +++ b/key-binding.h @@ -36,6 +36,8 @@ enum bind_action_normal { BIND_ACTION_SHOW_URLS_LAUNCH, BIND_ACTION_SHOW_URLS_PERSISTENT, BIND_ACTION_TEXT_BINDING, + BIND_ACTION_PROMPT_PREV, + BIND_ACTION_PROMPT_NEXT, /* Mouse specific actions - i.e. they require a mouse coordinate */ BIND_ACTION_SELECT_BEGIN, diff --git a/osc.c b/osc.c index 2abd3e86..55cfcf84 100644 --- a/osc.c +++ b/osc.c @@ -869,6 +869,40 @@ osc_dispatch(struct terminal *term) term->colors.use_custom_selection = term->conf->colors.use_custom.selection; break; + case 133: + /* + * Shell integration; see + * https://iterm2.com/documentation-escape-codes.html (Shell + * Integration/FinalTerm) + * + * [PROMPT]prompt% [COMMAND_START] ls -l + * [COMMAND_EXECUTED] + * -rw-r--r-- 1 user group 127 May 1 2016 filename + * [COMMAND_FINISHED] + */ + switch (string[0]) { + case 'A': + LOG_DBG("FTCS_PROMPT: %dx%d", + term->grid->cursor.point.row, + term->grid->cursor.point.col); + + term->grid->cur_row->prompt_marker = true; + break; + + case 'B': + LOG_DBG("FTCS_COMMAND_START"); + break; + + case 'C': + LOG_DBG("FTCS_COMMAND_EXECUTED"); + break; + + case 'D': + LOG_DBG("FTCS_COMMAND_FINISHED"); + break; + } + break; + case 555: osc_flash(term); break; diff --git a/terminal.c b/terminal.c index 4b1e5931..c449aa0e 100644 --- a/terminal.c +++ b/terminal.c @@ -1812,6 +1812,7 @@ erase_line(struct terminal *term, struct row *row) { erase_cell_range(term, row, 0, term->cols - 1); row->linebreak = true; + row->prompt_marker = false; } void diff --git a/terminal.h b/terminal.h index 17a5abf9..bf6e74fe 100644 --- a/terminal.h +++ b/terminal.h @@ -116,9 +116,13 @@ struct row_data { struct row { struct cell *cells; + struct row_data *extra; + bool dirty; bool linebreak; - struct row_data *extra; + + /* Shell integration */ + bool prompt_marker; }; struct sixel {