From 729cea88f3c2cb8642d73d83562cc648ac6bb773 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 22 Oct 2021 11:54:54 +0530 Subject: [PATCH] More sophisticated OSC 133 parsing with support for secondary prompts --- docs/shell-integration.rst | 8 +++- kitty/data-types.h | 4 +- kitty/line-buf.c | 14 ------ kitty/lineops.h | 2 - kitty/screen.c | 90 ++++++++++++++++++++++++++------------ kitty/screen.h | 3 +- 6 files changed, 72 insertions(+), 49 deletions(-) diff --git a/docs/shell-integration.rst b/docs/shell-integration.rst index 4bb5496a0..93fc17201 100644 --- a/docs/shell-integration.rst +++ b/docs/shell-integration.rst @@ -148,12 +148,16 @@ Notes for shell developers The protocol used for marking the prompt is very simple. You should consider adding it to your shell as a builtin. Many modern terminals make use of it, for -example: kitty, iTerm2, WezTerm +example: kitty, iTerm2, WezTerm, DomTerm -Just before starting to draw the prompt send the escape code:: +Just before starting to draw the PS1 prompt send the escape code:: 133;A +Just before starting to draw the PS2 prompt send the escape code:: + + 133;A;k=s + Just before running a command/program, send the escape code:: 133;C diff --git a/kitty/data-types.h b/kitty/data-types.h index 09fa30c16..dd2f45a22 100644 --- a/kitty/data-types.h +++ b/kitty/data-types.h @@ -164,12 +164,12 @@ typedef struct { hyperlink_id_type hyperlink_id; } CPUCell; +typedef enum { UNKNOWN_PROMPT_KIND = 0, PROMPT_START = 1, SECONDARY_PROMPT = 2, OUTPUT_START = 3 } PromptKind; typedef union LineAttrs { struct { uint8_t continued : 1; uint8_t has_dirty_text : 1; - uint8_t is_prompt_start : 1; - uint8_t is_output_start : 1; + PromptKind prompt_kind : 2; }; uint8_t val; } LineAttrs ; diff --git a/kitty/line-buf.c b/kitty/line-buf.c index d685aad7a..de2f1c9c9 100644 --- a/kitty/line-buf.c +++ b/kitty/line-buf.c @@ -57,20 +57,6 @@ linebuf_mark_line_as_not_continued(LineBuf *self, index_type y) { self->line_attrs[y].continued = false; } -void -linebuf_mark_line_as_prompt_start(LineBuf *self, index_type y) { - self->line_attrs[y].is_prompt_start = true; - self->line_attrs[y].is_output_start = false; -} - -void -linebuf_mark_line_as_output_start(LineBuf *self, index_type y) { - self->line_attrs[y].is_prompt_start = false; - self->line_attrs[y].is_output_start = true; -} - - - static PyObject* clear(LineBuf *self, PyObject *a UNUSED) { #define clear_doc "Clear all lines in this LineBuf" diff --git a/kitty/lineops.h b/kitty/lineops.h index 1a9b3dbe5..3dd040165 100644 --- a/kitty/lineops.h +++ b/kitty/lineops.h @@ -105,8 +105,6 @@ void linebuf_rewrap(LineBuf *self, LineBuf *other, index_type *, index_type *, H void linebuf_mark_line_dirty(LineBuf *self, index_type y); void linebuf_mark_line_clean(LineBuf *self, index_type y); void linebuf_mark_line_as_not_continued(LineBuf *self, index_type y); -void linebuf_mark_line_as_prompt_start(LineBuf *self, index_type y); -void linebuf_mark_line_as_output_start(LineBuf *self, index_type y); unsigned int linebuf_char_width_at(LineBuf *self, index_type x, index_type y); void linebuf_refresh_sprite_positions(LineBuf *self); void historybuf_add_line(HistoryBuf *self, const Line *line, ANSIBuf*); diff --git a/kitty/screen.c b/kitty/screen.c index 0fa76ee05..dd63cbd8b 100644 --- a/kitty/screen.c +++ b/kitty/screen.c @@ -157,7 +157,7 @@ screen_reset(Screen *self) { memset(self->main_key_encoding_flags, 0, sizeof(self->main_key_encoding_flags)); memset(self->alt_key_encoding_flags, 0, sizeof(self->alt_key_encoding_flags)); self->display_window_number = 0; - self->prompt_redraw_settings.val = 0; + self->prompt_settings.val = 0; self->last_graphic_char = 0; self->main_savepoint.is_valid = false; self->alt_savepoint.is_valid = false; @@ -284,17 +284,25 @@ index_selection(const Screen *self, Selections *selections, bool up) { static void prevent_current_prompt_from_rewrapping(Screen *self) { - if (!self->prompt_redraw_settings.redraws_prompts_at_all) return; + if (!self->prompt_settings.redraws_prompts_at_all) return; int y = self->cursor->y; while (y >= 0) { linebuf_init_line(self->main_linebuf, y); Line *line = self->linebuf->line; - if (line->attrs.is_output_start) return; - if (line->attrs.is_prompt_start) break; + switch (line->attrs.prompt_kind) { + case UNKNOWN_PROMPT_KIND: + break; + case PROMPT_START: + case SECONDARY_PROMPT: + goto found; + break; + case OUTPUT_START: + return; + } y--; } +found: if (y < 0) return; - if (!self->prompt_redraw_settings.redraws_multiline_prompts && self->cursor->y > (unsigned) y) return; // bash does not redraw multiline prompts // we have identified a prompt at which the cursor is present, the shell // will redraw this prompt. However when doing so it gets confused if the // cursor vertical position relative to the first prompt line changes. This @@ -322,7 +330,7 @@ screen_resize(Screen *self, unsigned int lines, unsigned int columns) { bool is_main = self->linebuf == self->main_linebuf; index_type num_content_lines_before, num_content_lines_after; bool dummy_output_inserted = false; - if (is_main && self->cursor->x == 0 && self->cursor->y < self->lines && self->linebuf->line_attrs[self->cursor->y].is_output_start) { + if (is_main && self->cursor->x == 0 && self->cursor->y < self->lines && self->linebuf->line_attrs[self->cursor->y].prompt_kind == OUTPUT_START) { linebuf_init_line(self->linebuf, self->cursor->y); if (!self->linebuf->line->cpu_cells[0].ch) { // we have a blank output start line, we need it to be preserved by @@ -1447,8 +1455,15 @@ screen_cursor_at_a_shell_prompt(const Screen *self) { if (self->cursor->y >= self->lines || self->linebuf != self->main_linebuf || !screen_is_cursor_visible(self)) return -1; for (index_type y=self->cursor->y + 1; y-- > 0; ) { linebuf_init_line(self->linebuf, y); - if (self->linebuf->line->attrs.is_output_start) return -1; - if (self->linebuf->line->attrs.is_prompt_start) return y; + switch(self->linebuf->line->attrs.prompt_kind) { + case OUTPUT_START: + return -1; + case PROMPT_START: + case SECONDARY_PROMPT: + return y; + case UNKNOWN_PROMPT_KIND: + break; + } } return -1; } @@ -1909,6 +1924,15 @@ file_transmission(Screen *self, PyObject *data) { CALLBACK("file_transmission", "O", data); } +static void +parse_prompt_mark(Screen *self, PyObject *parts, PromptKind *pk) { + for (Py_ssize_t i = 0; i < PyList_GET_SIZE(parts); i++) { + PyObject *token = PyList_GET_ITEM(parts, i); + if (PyUnicode_CompareWithASCIIString(token, "k=s")) *pk = SECONDARY_PROMPT; + else if (PyUnicode_CompareWithASCIIString(token, "redraw=0")) self->prompt_settings.redraws_prompts_at_all = 0; + } +} + void shell_prompt_marking(Screen *self, PyObject *data) { if (PyUnicode_READY(data) != 0) { PyErr_Clear(); return; } @@ -1916,20 +1940,21 @@ shell_prompt_marking(Screen *self, PyObject *data) { Py_UCS4 ch = PyUnicode_READ_CHAR(data, 0); switch (ch) { case 'A': { - linebuf_mark_line_as_prompt_start(self->linebuf, self->cursor->y); - size_t l = PyUnicode_GET_LENGTH(data); - self->prompt_redraw_settings.redraws_multiline_prompts = 1; - self->prompt_redraw_settings.redraws_prompts_at_all = 1; - if (l > 8) { - const char *buf = PyUnicode_AsUTF8(data); - if (buf) { - if (strstr(buf, ";does_not_redraw_multiline_prompt")) self->prompt_redraw_settings.redraws_multiline_prompts = 0; - if (strstr(buf, ";does_not_redraw_prompts")) self->prompt_redraw_settings.redraws_prompts_at_all = 0; + PromptKind pk = PROMPT_START; + self->prompt_settings.redraws_prompts_at_all = 1; + if (PyUnicode_FindChar(data, ';', 0, PyUnicode_GET_LENGTH(data), 1)) { + DECREF_AFTER_FUNCTION PyObject *sep = PyUnicode_FromString(";"); + if (sep) { + DECREF_AFTER_FUNCTION PyObject *parts = PyUnicode_Split(data, sep, -1); + if (parts) parse_prompt_mark(self, parts, &pk); } } + if (PyErr_Occurred()) PyErr_Print(); + self->linebuf->line_attrs[self->cursor->y].prompt_kind = pk; } break; case 'C': - linebuf_mark_line_as_output_start(self->linebuf, self->cursor->y); break; + self->linebuf->line_attrs[self->cursor->y].prompt_kind = OUTPUT_START; + break; } } if (global_state.debug_rendering) { @@ -1946,16 +1971,16 @@ screen_history_scroll_to_prompt(Screen *self, int num_of_prompts_to_jump) { num_of_prompts_to_jump = num_of_prompts_to_jump < 0 ? -num_of_prompts_to_jump : num_of_prompts_to_jump; int y = -self->scrolled_by; #define ensure_y_ok if (y >= (int)self->lines || -y > (int)self->historybuf->count) return false; -#define move_y_to_start_of_promt while (-y + 1 <= (int)self->historybuf->count && range_line_(self, y - 1)->attrs.is_prompt_start) y--; -#define move_y_to_end_of_promt while (y + 1 < (int)self->lines && range_line_(self, y + 1)->attrs.is_prompt_start) y++; +#define move_y_to_start_of_promt while (-y + 1 <= (int)self->historybuf->count && range_line_(self, y - 1)->attrs.prompt_kind == PROMPT_START) y--; +#define move_y_to_end_of_promt while (y + 1 < (int)self->lines && range_line_(self, y + 1)->attrs.prompt_kind == PROMPT_START) y++; ensure_y_ok; - if (range_line_(self, y)->attrs.is_prompt_start) { + if (range_line_(self, y)->attrs.prompt_kind == PROMPT_START) { if (delta < 0) { move_y_to_start_of_promt; } else { move_y_to_end_of_promt; } } while (num_of_prompts_to_jump) { y += delta; ensure_y_ok; - if (range_line_(self, y)->attrs.is_prompt_start) { + if (range_line_(self, y)->attrs.prompt_kind == PROMPT_START) { num_of_prompts_to_jump--; if (delta < 0) { move_y_to_start_of_promt; } else { move_y_to_end_of_promt; } } @@ -2583,8 +2608,8 @@ last_cmd_output(Screen *self, PyObject *args) { const int limit = -self->historybuf->count; while (y >= limit) { Line *line = range_line_(self, y); - if (line->attrs.is_prompt_start) prompt_pos = y; - if (line->attrs.is_output_start) { + if (line->attrs.prompt_kind == PROMPT_START) prompt_pos = y; + if (line->attrs.prompt_kind == OUTPUT_START) { oo.start = y; num_lines = prompt_pos - y; break; @@ -2599,7 +2624,7 @@ last_cmd_output(Screen *self, PyObject *args) { // so find the first one while (oo.start > limit) { Line *line = range_line_(self, oo.start - 1); - if (!line->attrs.is_output_start) break; + if (line->attrs.prompt_kind != OUTPUT_START) break; oo.start--; num_lines++; } } @@ -3519,8 +3544,19 @@ dump_lines_with_attrs(Screen *self, PyObject *accum) { PyObject_CallFunctionObjArgs(accum, t); Py_DECREF(t); } - if (line->attrs.is_prompt_start) PyObject_CallFunction(accum, "s", "\x1b[32mprompt \x1b[39m"); - if (line->attrs.is_output_start) PyObject_CallFunction(accum, "s", "\x1b[33moutput \x1b[39m"); + switch (line->attrs.prompt_kind) { + case UNKNOWN_PROMPT_KIND: + break; + case PROMPT_START: + PyObject_CallFunction(accum, "s", "\x1b[32mprompt \x1b[39m"); + break; + case SECONDARY_PROMPT: + PyObject_CallFunction(accum, "s", "\x1b[32msecondary_prompt \x1b[39m"); + break; + case OUTPUT_START: + PyObject_CallFunction(accum, "s", "\x1b[33moutput \x1b[39m"); + break; + } if (line->attrs.continued) PyObject_CallFunction(accum, "s", "continued "); if (line->attrs.has_dirty_text) PyObject_CallFunction(accum, "s", "dirty "); PyObject_CallFunction(accum, "s", "\n"); diff --git a/kitty/screen.h b/kitty/screen.h index 7da632552..50c518e7c 100644 --- a/kitty/screen.h +++ b/kitty/screen.h @@ -133,11 +133,10 @@ typedef struct { } ignore_bells; union { struct { - unsigned int redraws_multiline_prompts: 1; unsigned int redraws_prompts_at_all: 1; }; unsigned int val; - } prompt_redraw_settings; + } prompt_settings; unsigned int display_window_number; } Screen;