diff --git a/kitty/data-types.h b/kitty/data-types.h index 154f32917..a6c17e11b 100644 --- a/kitty/data-types.h +++ b/kitty/data-types.h @@ -41,6 +41,7 @@ void log_error(const char *fmt, ...) __attribute__ ((format (printf, 1, 2))); typedef unsigned long long id_type; typedef uint32_t char_type; typedef uint32_t color_type; +typedef uint32_t hyperlink_id_type; typedef uint16_t combining_type; typedef uint32_t pixel; typedef unsigned int index_type; @@ -160,6 +161,7 @@ typedef struct { typedef struct { char_type ch; combining_type cc_idx[2]; + hyperlink_id_type hyperlink_id; } CPUCell; diff --git a/kitty/hyperlink.c b/kitty/hyperlink.c new file mode 100644 index 000000000..98e30fff5 --- /dev/null +++ b/kitty/hyperlink.c @@ -0,0 +1,28 @@ +/* + * hyperlink.c + * Copyright (C) 2020 Kovid Goyal + * + * Distributed under terms of the GPL3 license. + */ + +#define _POSIX_C_SOURCE 200809L +#include +#include "screen.h" + +bool +parse_osc_8(char *buf, char **id, char **url) { + char *boundary = strstr(buf, ";"); + if (boundary == NULL) return false; + *boundary = 0; + if (*(boundary + 1)) *url = boundary + 1; + char *save, *token = strtok_r(buf, ":", &save); + while (token != NULL) { + size_t len = strlen(token); + if (len > 3 && token[0] == 'i' && token[1] == 'd' && token[2] == '=' && token[3]) { + *id = token + 3; + break; + } + token = strtok_r(NULL, ":", &save); + } + return true; +} diff --git a/kitty/line.c b/kitty/line.c index b7e69edef..a85c33413 100644 --- a/kitty/line.c +++ b/kitty/line.c @@ -558,7 +558,7 @@ line_get_char(Line *self, index_type at) { } void -line_set_char(Line *self, unsigned int at, uint32_t ch, unsigned int width, Cursor *cursor, bool UNUSED is_second) { +line_set_char(Line *self, unsigned int at, uint32_t ch, unsigned int width, Cursor *cursor, hyperlink_id_type hyperlink_id) { GPUCell *g = self->gpu_cells + at; if (cursor == NULL) { g->attrs = (self->gpu_cells[at].attrs & ATTRS_MASK_WITHOUT_WIDTH) | width; @@ -569,22 +569,24 @@ line_set_char(Line *self, unsigned int at, uint32_t ch, unsigned int width, Curs g->decoration_fg = cursor->decoration_fg & COL_MASK; } self->cpu_cells[at].ch = ch; + self->cpu_cells[at].hyperlink_id = hyperlink_id; memset(self->cpu_cells[at].cc_idx, 0, sizeof(self->cpu_cells[at].cc_idx)); } static PyObject* set_char(Line *self, PyObject *args) { -#define set_char_doc "set_char(at, ch, width=1, cursor=None) -> Set the character at the specified cell. If cursor is not None, also set attributes from that cursor." +#define set_char_doc "set_char(at, ch, width=1, cursor=None, hyperlink_id=0) -> Set the character at the specified cell. If cursor is not None, also set attributes from that cursor." unsigned int at, width=1; int ch; Cursor *cursor = NULL; + unsigned int hyperlink_id = 0; - if (!PyArg_ParseTuple(args, "IC|IO!", &at, &ch, &width, &Cursor_Type, &cursor)) return NULL; + if (!PyArg_ParseTuple(args, "IC|IO!I", &at, &ch, &width, &Cursor_Type, &cursor, &hyperlink_id)) return NULL; if (at >= self->xnum) { PyErr_SetString(PyExc_ValueError, "Out of bounds"); return NULL; } - line_set_char(self, at, ch, width, cursor, false); + line_set_char(self, at, ch, width, cursor, hyperlink_id); Py_RETURN_NONE; } diff --git a/kitty/lineops.h b/kitty/lineops.h index daeaa219b..618c0051f 100644 --- a/kitty/lineops.h +++ b/kitty/lineops.h @@ -70,7 +70,7 @@ typedef Line*(get_line_func)(void *, int); void line_clear_text(Line *self, unsigned int at, unsigned int num, char_type ch); void line_apply_cursor(Line *self, Cursor *cursor, unsigned int at, unsigned int num, bool clear_char); char_type line_get_char(Line *self, index_type at); -void line_set_char(Line *, unsigned int , uint32_t , unsigned int , Cursor *, bool); +void line_set_char(Line *, unsigned int , uint32_t , unsigned int , Cursor *, hyperlink_id_type); void line_right_shift(Line *, unsigned int , unsigned int ); void line_add_combining_char(Line *, uint32_t , unsigned int ); index_type line_url_start_at(Line *self, index_type x); diff --git a/kitty/parser.c b/kitty/parser.c index ce49d3471..8b4ed6fb8 100644 --- a/kitty/parser.c +++ b/kitty/parser.c @@ -121,6 +121,9 @@ _report_params(PyObject *dump_callback, const char *name, unsigned int *params, #define REPORT_OSC2(name, code, string) \ Py_XDECREF(PyObject_CallFunction(dump_callback, "sIO", #name, code, string)); PyErr_Clear(); +#define REPORT_HYPERLINK(id, url) \ + Py_XDECREF(PyObject_CallFunction(dump_callback, "szz", "set_active_hyperlink", id, url)); PyErr_Clear(); + #else #define DUMP_UNUSED UNUSED @@ -134,6 +137,7 @@ _report_params(PyObject *dump_callback, const char *name, unsigned int *params, #define FLUSH_DRAW #define REPORT_OSC(name, string) #define REPORT_OSC2(name, code, string) +#define REPORT_HYPERLINK(id, url) #endif @@ -302,6 +306,30 @@ handle_esc_mode_char(Screen *screen, uint32_t ch, PyObject DUMP_UNUSED *dump_cal // OSC mode {{{ +static inline void +dispatch_hyperlink(Screen *screen, size_t pos, size_t size, PyObject DUMP_UNUSED *dump_callback) { + // since the spec says only ASCII printable chars are allowed in OSC 8, we + // can just convert to char* directly + if (!size) return; // ignore empty OSC 8 since it must have two semi-colons to be valid, which means one semi-colon here + char *id = NULL, *url = NULL; + char *data = malloc(size + 1); + if (!data) fatal("Out of memory"); + for (size_t i = 0; i < size; i++) { + data[i] = screen->parser_buf[i + pos] & 0x7f; + if (data[i] < 32 || data[i] > 126) data[i] = '_'; + } + data[size] = 0; + + if (parse_osc_8(data, &id, &url)) { + REPORT_HYPERLINK(id, url); + set_active_hyperlink(screen, id, url); + } else { + REPORT_ERROR("Ignoring malformed OSC 8 code"); + } + + free(data); +} + static inline void dispatch_osc(Screen *screen, PyObject DUMP_UNUSED *dump_callback) { #define DISPATCH_OSC_WITH_CODE(name) REPORT_OSC2(name, code, string); name(screen, code, string); @@ -339,6 +367,9 @@ dispatch_osc(Screen *screen, PyObject DUMP_UNUSED *dump_callback) { START_DISPATCH DISPATCH_OSC_WITH_CODE(set_color_table_color); END_DISPATCH + case 8: + dispatch_hyperlink(screen, i, limit-i, dump_callback); + break; case 9: case 99: START_DISPATCH diff --git a/kitty/screen.c b/kitty/screen.c index 06951230a..b05d259e3 100644 --- a/kitty/screen.c +++ b/kitty/screen.c @@ -111,6 +111,7 @@ new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) { self->historybuf = alloc_historybuf(MAX(scrollback, lines), columns, OPT(scrollback_pager_history_size)); self->main_grman = grman_alloc(); self->alt_grman = grman_alloc(); + self->active_hyperlink_id = 0; self->grman = self->main_grman; self->pending_mode.wait_time = s_double_to_monotonic_t(2.0); @@ -143,6 +144,7 @@ screen_reset(Screen *self) { historybuf_clear(self->historybuf); grman_clear(self->grman, false, self->cell_size); self->modes = empty_modes; + self->active_hyperlink_id = 0; #define R(name) self->color_profile->overridden.name = 0 R(default_fg); R(default_bg); R(cursor_color); R(highlight_fg); R(highlight_bg); #undef R @@ -359,6 +361,15 @@ selection_has_screen_line(Selection *s, int y) { return top <= y && y <= bottom; } +void +set_active_hyperlink(Screen *self, char *id, char *url) { + (void)id; + if (!url || !url[0]) { + self->active_hyperlink_id = 0; + return; + } +} + static inline bool is_flag_pair(char_type a, char_type b) { return is_flag_codepoint(a) && is_flag_codepoint(b); } @@ -419,7 +430,7 @@ draw_combining_char(Screen *self, char_type ch) { CPUCell *cpu_cell = self->linebuf->line->cpu_cells + xpos; GPUCell *gpu_cell = self->linebuf->line->gpu_cells + xpos; if ((gpu_cell->attrs & WIDTH_MASK) != 2 && cpu_cell->cc_idx[0] == VS16 && is_emoji_presentation_base(cpu_cell->ch)) { - if (self->cursor->x <= self->columns - 1) line_set_char(self->linebuf->line, self->cursor->x, 0, 0, self->cursor, true); + if (self->cursor->x <= self->columns - 1) line_set_char(self->linebuf->line, self->cursor->x, 0, 0, self->cursor, self->active_hyperlink_id); gpu_cell->attrs = (gpu_cell->attrs & !WIDTH_MASK) | 2; if (xpos == self->columns - 1) move_widened_char(self, cpu_cell, gpu_cell, xpos, ypos); else self->cursor->x++; @@ -476,10 +487,10 @@ screen_draw(Screen *self, uint32_t och) { if (self->modes.mIRM) { line_right_shift(self->linebuf->line, self->cursor->x, char_width); } - line_set_char(self->linebuf->line, self->cursor->x, ch, char_width, self->cursor, false); + line_set_char(self->linebuf->line, self->cursor->x, ch, char_width, self->cursor, self->active_hyperlink_id); self->cursor->x++; if (char_width == 2) { - line_set_char(self->linebuf->line, self->cursor->x, 0, 0, self->cursor, true); + line_set_char(self->linebuf->line, self->cursor->x, 0, 0, self->cursor, self->active_hyperlink_id); self->cursor->x++; } self->is_dirty = true; @@ -646,6 +657,7 @@ screen_handle_graphics_command(Screen *self, const GraphicsCommand *cmd, const u void screen_toggle_screen_buffer(Screen *self, bool save_cursor, bool clear_alt_screen) { bool to_alt = self->linebuf == self->main_linebuf; + self->active_hyperlink_id = 0; grman_clear(self->alt_grman, true, self->cell_size); // always clear the alt buffer graphics to free up resources, since it has to be cleared when switching back to it anyway if (to_alt) { if (clear_alt_screen) linebuf_clear(self->alt_linebuf, BLANK_CHAR); diff --git a/kitty/screen.h b/kitty/screen.h index 8488ada3e..f0b0d589c 100644 --- a/kitty/screen.h +++ b/kitty/screen.h @@ -123,6 +123,7 @@ typedef struct { PyObject *marker; bool has_focus; bool has_activity_since_last_focus; + hyperlink_id_type active_hyperlink_id; } Screen; @@ -206,6 +207,8 @@ bool screen_history_scroll(Screen *self, int amt, bool upwards); Line* screen_visual_line(Screen *self, index_type y); unsigned long screen_current_char_width(Screen *self); void screen_mark_url(Screen *self, index_type start_x, index_type start_y, index_type end_x, index_type end_y); +bool parse_osc_8(char *buf, char **id, char **url); +void set_active_hyperlink(Screen*, char*, char*); void screen_handle_graphics_command(Screen *self, const GraphicsCommand *cmd, const uint8_t *payload); bool screen_open_url(Screen*); void screen_dirty_sprite_positions(Screen *self); diff --git a/kitty_tests/parser.py b/kitty_tests/parser.py index 8e54a2d78..6de47904c 100644 --- a/kitty_tests/parser.py +++ b/kitty_tests/parser.py @@ -227,6 +227,12 @@ class TestParser(BaseTest): pb('\033]9;test it\x07', ('desktop_notify', 9, 'test it')) pb('\033]99;moo=foo;test it\x07', ('desktop_notify', 99, 'moo=foo;test it')) self.ae(c.notifications, [(9, ''), (9, 'test it'), (99, 'moo=foo;test it')]) + c.clear() + pb('\033]8;;\x07', ('set_active_hyperlink', None, None)) + pb('\033]8moo\x07', ('Ignoring malformed OSC 8 code',)) + pb('\033]8;moo\x07', ('Ignoring malformed OSC 8 code',)) + pb('\033]8;id=xyz;\x07', ('set_active_hyperlink', 'xyz', None)) + pb('\033]8;moo:x=z:id=xyz:id=abc;http://yay;.com\x07', ('set_active_hyperlink', 'xyz', 'http://yay;.com')) def test_desktop_notify(self): reset_registry()